vue组件
概念
关于Web components组件化标准:
Vue/React模板系统是参考 Web components 规范来进行上层的设计,通过插槽/模板可以自定义组件,自定义标签,自定义属性,然后再渲染
HTML/DOM 已经规范了 Web components,经过特殊的编译,最终形成浏览器能够支持解析并渲染的一系列的代码,它本身是有一套支持浏览器(不需工程化)的组件化系统/规范
存在目的:
希望有这种方案提供给开发者能自定义可重用的,可被浏览器正常解析的标签,让逻辑样式标签被封装在一个组件中,最终用自定义标签渲染视图
标签提供了
template,slot容器是
shadowDOM方法是
customElements.define自定义元素的方法
//shadowDom//自定义标签<my-infoavator=""name="kevin"age="28">The information of kevin</my-info>
通过window.customElements.define实现1(通过JS创建):
window.customElements.define('my-info', class extends HTMLElement {constructor(){super();console.log(this);//<my-info>...</my-info>this.title = this.textContent;this.avatar = this.getAttribute('avatar');this.myName = this.getAttribute('name');this.age = this.getAttribute('age');this.init();}init(){//装入shadowRoot里,形成shadowDOM//API: attachShadow()//mode: 往后是否再对shadowDOM进行操作var shadowDOM = this.attachShadow({ mode: 'open' });shadowDOM.appendChild(this.createDom());}//创建节点createDom(){var oContainer = this.createContainer();oContainer.appendChild(this.createTitle());oContainer.appendChild(this.createAvatar());oContainer.appendChild(this.createName());oContainer.appendChild(this.createAge());return oContainer;}//创建一个大的容器createContainer(){var oContainer = document.createElement('div');oContainer.className = 'my-info-container';return oContainer;}//创建标签createTitle(){var oTitle = document.createElement('h1');oTitle.className = 'my-info-title';oTitle.textContent = this.title;return oTitle;}createAvatar(){...}createName(){...}createAge(){...}});//最后浏览器显示页面,标签也是自定义的标签<div class="my-info-container">#shadow-root(open)...<div>...</div></div>
通过window.customElements.define实现2(通过template/slot标签创建):
<template id="my-article-template"><div class="my-article"><h1 class="my-article-title">//slot插槽,相当于vue的占位<slot name="title" class="title"></slot><slot name="author" class="author"></slot><slot name="dateTime" class="dateTime"></slot></h1><p class="my-article-content"><slot name="content"></slot></p></div></template>//以上会显示在DOM树,但不会渲染到页面//所以定义一个自定义标签window.customElements.define('my-article', class extends HTMLElement {constructor() {super();var _tpl = document.getElementById('my-article-template').content;var shadowDOM = this.attachShadow({mode: 'open'});//cloneNode(true) 把tpl里面的content复制一份(true:包括子元素)shadowDOM.appendChild(_tpl.cloneNode(true));}});//在html里使用<div id="app"><my-article><p slot="title">This is my Title</p><span slot="author">kevin</span><span slot="dateTime"> 10:15 </span><p slot="content">This is my Content</p></my-article></div>//此时浏览器页面渲染成功显示内容
组件化存在的必要性:
Vue组件化核心是组件化系统,利用 ES模块化来完成 vue组件系统的构建
原理:
导入,导出,一个组件就是一个模块,将所有整个页面上的各个单元抽离成各个小的单元,通过模块化组合在一起,最终用 viewmodel组装成一个真实的 DOM树
组件化作用:
- 组件化是抽象了一个小型,独立,可预先定义配置的,可复用的组件
- 组件最大的作用是独立开发,预先配置,为了更好的维护和拓展
组件树:
组件的嵌套形成了一个组件树
- 小型化:页面的构成拆分成一个一个的小单元
- 独立性:独立开发,每一个小单元尽可能独立
- 预定性:预先定义,每一个小单元都可以先定义好再需要的时候导入使用
- 配置化:预先配置,小单元可以接收一些在使用的时候需要的一些配置
- 可复用:小单元在多个地方可以使用
注意:可复用性要适当的考量,有一些组件确实是不需要服用的,可配置性越高,功能性就越强
问题:组件化带来了什么好处?
组件可以看成一个独立的块,在任意的地方多次使用(复用性),独立使用决定了维护性高,高配置度,提供了接口,让用户传入一些属性,配置宣高,使用的多样性
希望一个页面的每个部分单独分离成一个小切片
- 每个切片都有自己的视图结构,样式,逻辑
- 每个切片形成的结构,样式,逻辑的整体成为组件
组件可以相互嵌套,组件内部可以使用组件
组件实例
问题:组件逻辑的本质是什么?
组件实例是一个对象,里面有很多特定的属性,每个组件都有自己的组件实例,一个应用中所有的组件都共享一个应用实例
无论是根组件还是应用内其他的组件,配置选项,组件行为都是一样的(生命周期函数)
Vue组件文件结构:
<template><!-- 组件模板区域 --></template><script>// 组件逻辑区域 逻辑模块export default {},};</script><style lang="scss">// 组件样式区域</style>
好处:
提高开发效率,可重复使用,简化调试步骤,提升整个项目的可能性,便于协同开发
根组件实例
根组件的本质是一个对象,createApp()执行的时候需要一个根组件,所以执行createApp({})里面至少放一个对象
根组件是 Vue渲染的起点,创建顶层树的节点
const RootComponent = {data(){return {a: 1,b: 2}},methods: {plus(){return this.a + this.b;},template: `<h1>{{a}}</h1>`}}const app = Vue.createApp(RootComponent);//mount方法执行返回的是根组件实例即RootComponent对象//vm是ViewModel简称,来源MVVM里的VM//补充:Vue不是一个完整的MVVM模型,只是作为参考const vm = app.mount('#app');console.log(vm);//打印的是RootComponent实例对象里面的属性和方法
问题:根元素是什么?
是一个 HTML 的元素,createApp()执行创建 Vue应用实例时,需要 HTML根元素
总结:
- 组件实例可以添加一些属性
property vue自定义添加的属性有data/props/components/methods...vue组件实例内置的方法有$attrs/$emit…
组件注册
区别:
全局注册不需要导入的(单独注册),而局部注册必要要注册
写法:
//通过app.component(组件名称,组件本身)的方式实现全局组件注册Vue.component(组件名称, {data: 组件数据,template: 组件模板内容})//局部组件注册:var ComponentA = {/* ... */};var ComponentB = {/* ... */};var ComponentC = {/* ... */};new Vue({el: "#app",// 注册局部组件components: {// 局部组件名称 : 组件内容'component-a': ComponentA,'component-b': ComponentB,'component-c': ComponentC}})
问题:如何设定 name属性?
在 Vue中,推荐组件名和使用组件时所用的标签名尽量一致原则
//在哪个组件树需要节点,就在哪注册组件app.component('root', {name: 'MyTitle',components: {MyTitle},template: `<my-title/>`;});
组件定义(推荐使用大驼峰)方式:
<my-title>是kebab-case,符合W3C对标签使用的规范(XHTML),避免现有或将来的HTML标签的冲突,避免有些大小写不敏感的文件系统,MyTitle是PascalCase大驼峰,有利于编辑器代码补全,JSX使用进行标签书写的,缺点是组件名称是驼峰的话HTML是不会处理的
组件数据
数据绑定:数据与视图渲染之间的关系
React:单向数据绑定,通过事件event触发state的更改导致视图变更Vue:双向数据绑定- 通过事件
event触发state的更改导致视图变更 v-model机制可以完成视图变化导致state/data变更
- 通过事件
数据流:就是数据流淌的方向,父子组件中数据按照什么方向流动
React和Vue都是单向数据流,父组件传递state给子组件作为props- 子组件不能
props变更导致父组件的state变更 - 只能是父组件的
state变更导致子组件的props变更
组件化
组件树结构如下:
AppMyHeaderMyLogoMyNavNavItemMyUser
可以看出结构树存在缺点:
深度组件化显得越来越复杂,维护低效率,解决办法是让组件化设计尽量的扁平化,拆分组件,实现组件独立,配置性高,容易维护
组件插槽
是组件化当中一个形象的一种占位,内容占位标签,组件的扩展功能
slot可以被替换为普通文本,也可以被替换为 html标签,也可以被替换为组件,也可以被替换为组合。
//app.vue<div><my-button>普通文本/html标签/组件</my-button></div>
//MyButton组件<button><!-- slot标签之间的内容是默认值 --><slot>Click</slot><button>
问题:多个插槽如何找到对应的插槽?
使用具名插槽v-slot="xxx",简写#xxx,注意v-slot只能用在<template>上
匿名插槽可以跟具名插槽一起使用,匿名插槽默认写法#default/v-slot:default
案例:后台管理系统页面布局
通过插槽替换页面布局区域的内容
//布局:BaseLayoutBaseHeader -> slot -> 命名:baseHeaderBaseFooter -> slot -> 命名:baseFooterBaseSideBar -> slot -> 命名:baseSideBarBaseMain -> slot -> 命名:baseMain
//项目结构:├─src| ├─App.vue| ├─main.js| ├─components| | ├─MainLogo| | | └index.vue| | ├─MainBoard| | | └index.vue| | ├─FooterContent| | | └index.vue| | ├─BaseList| | | └index.vue| | ├─BaseLayout| | | └index.vue
//布局结构<div class="container"><header class="base-header"><slot name="baseHeader">HEADER</slot></header><footer class="base-footer"><slot name="baseFooter">FOOTER</slot></footer><aside class="base-sidebar"><slot name="baseSideBar">SIDEBAR</slot></aside><main class="base-main"><!-- 这里是匿名slot 默认不用命名 --><slot>MAIN</slot></main></div>
//app.vue//通过vslot占位//注意v-slot只能用在<template>上<template><div><base-layout><template #baseHeader><main-logo></main-logo></template><template #baseFooter><footer-content></footer-content></template><template #baseSideBar><base-list></base-list></template><template #default><main-board></main-board></template></base-layout></div></template>
源码地址:
https://gitee.com/kevinleeeee/admin-system-layout-slot-demo
插槽作用域:
//父组件定义内容//props.item 相当于 子组件传递过来的item<pic-board><template v-slot:default="props">{{props.item}}{{props.field}}</template></pic-board>//子组件占位并传值<div><ul><li v-for="item of picData" :key="item.id"><div><h1>{{item.title}}</h1><slot :item="item" :field="1"></slot></div></li></ul></div>
//解构写法:<pic-board><template #default="{url: 别名, desc: description, field=默认值}"><img :src="imgUrl" :alt="description"/></template></pic-board>
案例:树型结构组件和组件递归
写一个左侧边栏鼠标移动显示/隐藏子菜单,且子菜单内容是根据数据的多维结构进行递归显示
技术:
vue2.x/组件递归/动态插槽
//项目结构├─package.json├─webpack.config.js├─src| ├─App.vue| ├─main.js| ├─libs| | ├─jspp-ui| | | ├─index.js - TreeMenu UI插件入口配置文件| | | ├─TreeMenu| | | | ├─index.vue| | | | ├─MenuItem.vue - 没有子项的菜单组件| | | | ├─ReSubMenu.vue - 包括有没有子项的递归菜单组件| | | | └SubMenu.vue - 有子项的菜单组件| ├─data| | └menu.js - 数组里保存多个多维的对象数据├─public| └index.html
源码地址:
https://gitee.com/kevinleeeee/aside-list-and-subaside-list-tree-demo
组件通信
遵循数据单向流原则,父传子组件通过绑定视图属性,子传父通过$emit事件逐层向上传递
子组件的 props: 接收父组件传递过来的数据
//接收格式:`props['自定义接收名称']`Vue.component('menu-item',{props: ['title'],template: '<div>{{ title }}</div>'})
父组件通过属性将值传递给子组件
//传递格式:`v-bind:自定义接收名称='要传递的数据'`<menu-item title="来自父组件的数据"></menu-item><menu-item :title="title"></menu-item>
父子组件方法传递,在父组件中通过 v-on传递方法
//传递格式:`v-on:自定义接收名称="要传递方法"`
在子组件中自定义一个方法
在自定义方法中通过`this.$emit('自定义接收名称')`触发传递过来的方法
子组件向父组件传值
this.$emit('需要调用的函数名称', '给调用的函数传递的参数')
单向数据流:只允许父组件向子组件传递数据,而不允许子组件直接操作 props中的数据
Provide/Inject
在组件树中,组件之间存在依赖关系,但有数据传递的时候需要父子组件注册
一旦有嵌套很深的组件时会存在弊端:
- 单向数据流的关系导致组件传递数据会存在强制注册属性
- 还有许多中间组件只有注册的数据,但并未使用数据
Provide Inject 就是解决以上问题,在提供数据的父组件通过 Provide把数据提供出来,然后组件下面的所有的子组件不管层级多深,数据都可以直接穿透(注入)任何的组件关系,让子组件能够直接的使用数据
Provide Inject存在弊端:
- 父组件
provide数据,子组件无论哪个层级的组件它用Inject注入,但是数据绑定的时候并不是响应式的(默认情况) - 父组件是不知道哪个组件使用了
Provide的数据 - 子组件也不知道哪个组件提供了数据,无法查询数据来源
所以,最好的使用场景是:
- 在一个组件体系下,如果有深度嵌套的时候
- 在一个组件体系下,多层级多个组件使用的时候
//例子1:在一个组件体系下,如果有深度嵌套的时候组件:Page -> SideBar -> List -> Item -> Link传递index属性 props接收index并provide ------------------> inject接收
//例子2:在一个组件体系下,多层级多个组件使用的时候TodoList -> TodoFooter -> TodoStasticstodolist -> todos -> Itemtodolist -> len -> lenProvide len -------------> len
总结:
Provide/Inject可以使用但不能滥用,最好在 Inject使用时注释一下数据来源
案例:todolist
实现一个 Todolist,组件之间通过 Provide/Inject/props 传递数据,子组件逐层传递事件到父组件来改变 todoList数组实现增删
技术:
vue2.x/provide/inject/ref
//项目目录├─package.json├─webpack.config.js├─src| ├─App.vue| ├─main.js| ├─components| | ├─TodoList| | | ├─index.vue - 主组件管理todoList数组/provide响应式数据/操作todoList数组| | | ├─Todos - todolist列表项的内容组件/绑定事件传递/inject绑定属性/遍历子项/按需渲染组件| | | | ├─index.vue| | | | ├─TodoCheck.vue - 每条内容的左侧复选框组件/事件传递/绑定属性| | | | ├─TodoContent.vue - 每一项内容组件/绑定属性| | | | ├─TodoItem.vue - todolist遍历到的每一项的组件/事件传递/绑定属性| | | | └TodoRemove.vue - 每一项内容右侧的删除按钮组件/事件传递/绑定属性| | | ├─TodoHeader| | | | ├─index.vue - 头部组件/事件传递并整合数据为对象/绑定属性/拿到Input里的方法(清空输入框文本内容)| | | | ├─TodoButton.vue - 增加按钮组件/事件传递| | | | └TodoInput.vue - 输入框组件/绑定属性/监听输入内容数据并事件传递| | | ├─TodoFooter| | | | ├─index.vue 底部组件/事件传递| | | | ├─TodoClear.vue - 全部删除组件/事件传递| | | | └TodoInfo.vue - todolist内容条数信息组件/inject绑定属性├─public| └index.html
问题 1:在使用 provide的组件里不能直接拿到 data里定义的属性
解决方案是将 provide写成函数的形式而不是对象的形式,函数内部返回一个新的对象引用避免数据同时被修改
问题 2:默认不会修改 placeholder属性,没有数据响应式
因为vue2.x 里明确表示少用provide/inject的方式穿透传递属性,而是通过父子间传递(遵从单向数据流体系)
解决方案 1:
利用vue3.x组合式 API里Vue.computed(()=>this.placeholder),这方法返回一个 ComputedRefImpl对象,该对象里的 value属性对应 placeholder的值
解决方案 2:
在vue2.x 中,在 provide函数返回的对象里直接把 vue实例传递过去TodoListIns: this,且在 Inject的组件里写inject: ["TodoListIns"],这里底下组件拿到的是整个组件 vue实例 VueComponent{...},而实例底下的 placeholder属性就是拿到的值
问题 3:如何让父组件知道子组件数据被修改了?
解决方案是子组件通过 watch监听 data定义的属性的变化, 向父组件传递事件,并把该属性传递过去,在父组件(逐层传递)中模板标签中绑定自定义事件,并在 methods里定义该事件, 把子组件传递的输入框内容并保存在当前组件里
问题 4:如何在父组件中找到子组件 methods里定义的方法?
解决方案是父组件通过模板中标签绑定 ref属性到子组件,然后通过this.$refs.子组件名称.子组件定义的方法()找到,原理是refs保存着被绑定子组件的组件实例
项目总结:在深度化组件的项目里,组件设计之初,应该减少组件的嵌套的原则,否则逐层的事件向上传递或者逐层的数据向下传递会增加代码的冗余且不便于阅读,尽可能少使用Provide/Inject,适用于上层组件 provide带响应式数据的组件实例,底层组件 inject穿透使用(最后注释数据来源的组件)
源码地址:
https://gitee.com/kevinleeeee/todolist-vue-provide-inject-demo
动态组件
动态组件在交互中,组件的渲染是不确定的,根据交互的操作来决定渲染哪个组件
关于异步组件:
没有必要在当前进行加载的组件称为异步组件,好处是不会打包在项目里,是按需从服务器上下载并加载
写法:
<component :is=""></component>
定义异步组件写法:
//在vue3.x定义的方法,必须用defineAsyncComponent()方法AsyncComp: defineAsyncComponent(() => {new Promise((resolve, reject) => {resolve({//定义组件 都为异步组件data(){return {...}},template: `...`});});});//在vue2.x定义,直接用import把路径放入AsyncComp: () => import('./xxx');
案例:登录页面
一个登录页面的登录框里有账号密码,扫二维码,还有手机号登录,切换登录方式
原理:
默认账号密码登录为组件加载,其他登录方式为异步组件加载
组件切换时,缓存组件会保持组件的状态,避免反复渲染导致性能问题
技术:
vue2keep-alive组件defineAsyncComponent()
问题:如何实现缓存AccountLogin组件的状态保证切换时不会丢失用户名和密码输入框信息?
通过<keep-alive>进行组件包裹
<keep-alive><component :is="currentTabComponent"></component></keep-alive>
问题:keep-alive是做什么的?
使用keep-alive包裹以下3个组件,希望组件输入的信息和状态是保存下来的:
- 账号密码登录 -> 首次加载组件
- 扫二维码 -> 异步加载组件
- 手机号登录 -> 异步加载组件
问题:异步组件在项目中的作用是什么?
在很多时候,其实没有必要在当前进行加载的组件定义为异步组件,好处是并不打包在总的打包文件里面,会分割成代码块文件,当需要加载的时候它会从服务器上按需下载并加载,大大减少主打包文件的体积,大大提升打包的速度
目录结构:
//项目结构:├─package.json├─webpack.config.js├─src| ├─App.vue| ├─main.js| ├─components| | ├─MainLogin| | | ├─AccountLogin.vue - 账号密码登录组件| | | ├─index.vue - 登录组件容器/keep-alive实现缓存组件状态/异步按需加载组件| | | ├─MobileLogin.vue - 手机登录组件| | | └QrcodeLogin.vue - 扫二维码登录组件| ├─assets| | ├─img| | | └qrcode.jpg├─public| └index.html
源码地址:
https://gitee.com/kevinleeeee/async-component-load-demo
案例:动态切换组件
点击 nav里面的 item时切换当前组件(动态)的 page信息
作者列表/文章列表/图文列表 组件
准备数据:
//RecommentTap数据:一个二维数组[//第一组数据[{id: 1,title: 'xxx',author: 'xxx',dateTime: 'xxx'},{id: 2,title: 'xxx',author: 'xxx',dateTime: 'xxx'},{id: 3,title: 'xxx',author: 'xxx',dateTime: 'xxx'}],//第二组数据[{id: 1,title: 'xxx',author: 'xxx',dateTime: 'xxxx',imgUrl: 'xxx',statu: 0},{id: 2,title: 'xxx',author: 'xxx',dateTime: 'xxxx',imgUrl: 'xxx',statu: 0},{id: 3,title: 'xxx',author: 'xxx',dateTime: 'xxxx',imgUrl: 'xxx',statu: 0}],//第三组数据[{id: 1,name: 'xxx',imgUrl: 'xxx'},{id: 2,name: 'xxx',imgUrl: 'xxx'},{id: 3,name: 'xxx',imgUrl: 'xxx'}]]
//基于2.x版本//组件结构:- RecommendTap- index.vue- item.vue- List- ArticleList- AuthorList- ImageTextList
//RecommendTap > index.vue<template><div class="tab"><div class="nav"><recommend-itemv-for="(item, index) of tabData":key="index":title="item.title":my-index="index":current-index="currentIndex"@change-index="changeIndex"></recommend-item></div></div><div class="list">//vue内置组件提供is属性可以确定当前组件是哪一个<component :is="currentComponent"></component></div<</template>export default {name: 'recommend-tab',props: ['initialIndex'],components: {RecommendItem}compute: {//利用beData创造选项卡需要的datatabData(){return [{title: '推荐软文',data: this.beData[0],component: 'article-list'},{title: '推荐图文',data: this.beData[1],component: 'image-text-list'}]},//如何准确找到当前组件作为动态组件?currentComponent(){return this.tabData[this.currentIndex].component;}},mounted(){//证明越界 解决currentIndex问题this.currentIndex =this.initialIndex > this.beData.length - 1? 0: this.initialIndex;},data(){return {currentIndex: 0,//模拟后端的数据beData: [//RecommentTap数据:一个二维数组...]}},methods:{changeIndex(index){this.currentIndex = index;}}}
//recommend-item组件:export default {name: 'recommend-item',props: ['title','myIndex','currentIndex'],methods: {//操作父组件的currentIndex数据changeIndex(index){this.$emit('change-index', index);}}}
