这是我参与更文挑战的第4天,活动详情查看: 更文挑战前端
自定义组件可以帮咱们更好的复用代码和重构简化代码复杂度。从小程序基础库版本 1.6.3 开始,小程序支持简洁的组件化编程。全部自定义组件相关特性都须要基础库版本 1.6.3 或更高。开发者能够将页面内的功能模块抽象成自定义组件,以便在不一样的页面中重复使用;也能够将复杂的页面拆分红多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件很是类似。node
小程序自定义组件的开发细节的核心点:web
-组件的生命周期;编程
在这个过程当中会结合组件化思想,以及 Web Components 规范辅助理解。Web Components 规范是 W3C 推出的一套用于封装具备复用性、互动性前端组件的技术规范,旨在提供一种标准的组件化模式。 目前流行的几个前端框架( Vue / React / Angular )都在必定程度上遵循了这套规范,微信小程序的自定义组件也是如此,并且,微信小程序渲染自定义组件使用的是 Shadow DOM,这项技术是 Web Components 规范的一部分。 做为一名前端开发者,你可能经历过 BootStrap 盛行的年代,也多是从更悠久的 ExtJS 时代一路走来,就算你不了解这两个框架,确定不可避免地使用过 React、Vue 或 Angular 这三种框架。React / Vue / Angular 与它们的前辈 BootStrap / ExtJS 有一点是共通的:它们都是前端组件化的推崇者。 能够把“前端组件化”理解为“面向对象编程思想在前端 UI 领域的具体实践,将部分 UI 内容抽离为独立的可复用的组件”,这样作有这样 3 点优点:json
这是组件化最直接的优势,若是不用组件,每次遇到相同的业务场景都须要从新编写代码,而被抽离后的组件能够在其适用的场景内被重复使用,很大程度上下降了开发耗时。小程序
想象一下,假如一个页面的全部 UI 源码都集中在同一个 HTML 文件中,当页面中的导航栏出现 Bug,你须要在上千行甚至上万行的 HTML 文件中找到导航栏对应的 HTML 标签。若是将导航栏抽离为一个组件,那么你仅仅须要在这个组件内寻找。这类案例在工做中广泛存在,经过这个例子能够充分说明组件化在下降代码维护难度方面的优点。微信小程序
《重构:改善既有代码的设计》讲了:重构并非当系统复杂度提高到必定程度难以维护时的一次性行为,而是一种高频的、小规模的平常行为。直白点儿说就是:应该不断经过重构来改善系统,无论重构的范围有多小。可是这种实践方式对于代码的可维护性有很大的挑战,这也间接说明了组件化对重构工做的正面影响:经过提升代码的可维护性,间接下降了系统的重构难度。数组
Component像页面同样由wxml、wxss、js和json4个文件组成,且须要把这4个文件放在同一个目录中。与页面不同的是,Component中的构造函数(也能够称构造器)是Component({}),而页面中的构造函数是Page({})。要编写一个自定义组件,首先须要在 json 文件中进行自定义组件声明(将component字段设为true可这一组文件设为自定义组件):浏览器
{
"component": true
}
复制代码
Component的slot(slot意思是插槽),主要是让你在外部的wxml能够自由的在你的Component的wxml里插入模块。默认状况下,一个组件的wxml只可能有一个slot。须要使用多个时,能够在组件js中声明启用。前端框架
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
properties: { /* ... */ },
methods: { /* ... */ }
})
复制代码
此时,能够在这个组件的wxml中使用多个slot,以不一样的 name 来区分。
<!-- 组件模板 -->
<view class="wrapper"> <slot name="before"></slot> <view>这里是组件的内部细节</view> <slot name="after"></slot> </view>
复制代码
使用时,用 slot 属性来将节点插入到不一样的slot上。
<!-- 引用组件的页面模板 -->
<view>
<component-tag-name>
<!-- 这部份内容将被放置在组件 <slot name="before"> 的位置上 -->
<view slot="before">这里是插入到组件slot name="before"中的内容</view>
<!-- 这部份内容将被放置在组件 <slot name="after"> 的位置上 -->
<view slot="after">这里是插入到组件slot name="after"中的内容</view>
</component-tag-name>
</view>
复制代码
组件和引用组件的页面不能使用id选择器(#a)、属性选择器([a])和标签名选择器,请改用class选择器。组件和引用组件的页面中使用后代选择器(.a .b)在一些极端状况下会有非预期的表现,如遇,请避免使用。子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其余组件可能致使非预期的状况。
继承样式,如 font 、 color ,会从组件外继承到组件内。除继承样式外, app.wxss 中的样式、组件所在页面的的样式对自定义组件无效 (小程序频繁报大量的警告)。
#a { } /* 在组件中不能使用 */
[a] { } /* 在组件中不能使用 */
button { } /* 在组件中不能使用 */
.a > .b { } /* 除非 .a 是 view 组件节点,不然不必定会生效 */
复制代码
使用外部样式类可让组件使用指定的组件外样式类,若是但愿组件外样式类可以彻底影响组件内部,能够将组件构造器中的options.addGlobalClass字段置为true。
/* 组件 custom-component.js */
Component({
externalClasses: ['my-class']
})
<!-- 组件 custom-component.wxml -->
<custom-component class="my-class">这段文本的颜色由组件外的 class 决定</custom-component>
/* 组件外的样式定义 */
.red-text {
color: red;
}
复制代码
建立一个组件
<!--components/component/component.wxml-->
<view class="inner"> {{innerText}} </view>
<slot></slot>
复制代码
编写JS文件,组件的属性值和内部数据将被用于组件 wxml 的渲染,其中,属性值是可由组件外部传入的
// components/component/component.js
Component({
/** * 组件的属性列表 */
properties: {
innerText: {
type: String,
value: 'hello world'
},
myProperties:String
},
/** * 组件的初始数据 */
data: {
},
/** * 组件的方法列表 */
methods: {
}
})
复制代码
设置字体的颜色
/* components/component/component.wxss */
.inner{color: red;}
复制代码
完成对组件的初始化,包括设置属性列表,初始化数据,以及设置相关的方法。
使用已注册的自定义组件前,首先要在页面的 json 文件中进行引用声明。此时须要提供每一个自定义组件的标签名和对应的自定义组件文件路径:
{
"usingComponents": {
"component": "/components/component/component"
}
}
复制代码
在page页面下添加声明过的自定义组件:
// <component></component>
<view>
<component> <!-- 这部份内容将被放置在组件 <slot> 的位置上 --> <view>这里是插入到组件slot中的内容</view> </component> </view>
复制代码
上方的是一个最简单的自定义组件。在开发微信小程序自定义组件的三个核心环节中咱们须要注意如下几个细节:
建立微信小程序自定义组件须要使用 Component 构造器,这是微信小程序结构体系内最小粒度的构造器,外层是 Page 构造器,最外层的是 App 构造器,三者的关系以下图:
从外到内依次是 App > Page > Component,每次递进是 1:N 的关系:
1 个 App(也就是 1 个小程序)可包含 N( N >= 1 )个 Page;
1 一个 Page 可包含N(N>=1)个 Component。
每一个自定义组件的资源必须包括四个基本文件:
用于描述组件结构的 wxml 文件;
用于描述组件样式的 wxss 文件;
用于描述组件行为的 js 文件;
用于声明组件配置的 json 文件。
跟传统前端开发相比,小程序自定义组件的 wxml 和 wxss 文件的编写方式与 HTML 和 CSS 编写基本相似,不要特别关注,差别性主要体如今 js 和 json 文件上,在 json 文件中必须经过 component 字段声明此组件为自定义组件,以下:
{
"component": true
}
复制代码
js 文件中经过 Component 构造器建立组件的逻辑实体,以下:
Component({
behaviors:[],
properties:{},
data: {},
lifetimes: {},
pageLifetimes: {},
methods: {}
});
复制代码
对照 Vue 和 React 讲解 Component 构造器的几个属性更容易理解: behaviors 相似于 Vue 和 React 中的 mixins,用于定义多个组件之间的共享逻辑,能够包含一组 properties、data、lifetimes 和 methods 的定义;
properties
相似于 Vue 和 React 中的 props ,用于接收外层(父组件)传入的数据;
data
相似于 Vue 中的 data 以及 React 中的 state ,用于描述组件的私用数据(状态);
lifetimes
用于定义组件自身的生命周期函数,这种写法是从小程序基础库 2.2.3 版本引入的,本来的写法与 Vue 和 React 相似,都是直接挂载到组件的一级属性上(下一小节咱们将详细讲解生命周期函数的相关知识);
pageLifetimes
是微信小程序自定义组件首创的一套逻辑,用于监听此组件所在页面的生命周期。通常用于在页面特定生命周期时改变组件的状态,好比在页面展现时(show)把组件的状态设置为 A,在页面隐藏时(hide)设置为 B;
methods 与 Vue 的 methods 相似,用于定义组件内部的函数。
除 4 个基础文件之外,自定义组件还能够包含一些其余必要的资源,好比图片,下图展现的是自定义组件 chatroom 的资源列表:你能够看到,除了 wxml/wxss/js/json 文件之外,还有两个图片文件,在 wxml 中能够直接使用相对目录引用,
<image src="./photo.png"></image>
复制代码
而对一个组件来讲,生命周期指的是这个组件从被建立到销毁的过程,在这个过程当中的里程碑阶段暴露出一些钩子函数,方便开发者针对不一样阶段编写逻辑,这些函数就是所谓的“生命周期函数”。微信小程序自定义组件的生命周期函数有如下几个:
跟 Vue 和 React 相比,小程序自定义组件的生命周期更贴近 Web Components 规范。因此接下来咱们结合 Web Components 规范来理解小程序自定义组件的生命周期。 Web Components 规范引入了一个概念:自定义 HTML 元素。目的跟小程序相似,都是为了建立一种自定义的 UI 组件。浏览器环境中,每一个 HTML 标签都存在一个对应的类(Class),好比段落节点 对应 HTMLParagraphElement 类,继承这个类所建立的元素即是自定义 HTML 元素,以下代码:
// 建立自定义元素
class MyCustomParagraphElement extends HTMLParagraphElement {
//...
}
// 注册自定义元素
customElements.define('custom-p', MyCustomParagraphElement);
复制代码
自定义元素必须被注册(或者叫做定义)以后才能够被使用,上述代码的最后一行即是注册逻辑,第一个参数是该元素被注册后的 HTML 标签名称。注册成功后即可以直接在 HTML 中使用该元素,以下:
<custom-p></custom-p>
复制代码
这个流程与微信小程序的自定义组件很是类似,只不过注册组件的行为是由小程序底层处理的,开发者仅须要编写组件自己的代码就能够了。 Web Components 规范对于自定义 HTML 元素的生命周期描述为下图所示的流程:
对比 Web Components 规范和小程序自定义组件的生命周期,二者有必定类似之处但并不彻底一致,总结出这样几点:
小程序自定义组件的 attached 和 detached 函数分别对应 Web Components 规范的connectedCallback 和 disconnectedCallback,功能上是一致的;
小程序自定义组件的 moved 函数与 Web Components 规范的 adoptedCallback 相似但做用并不彻底相同。因为小程序不支持 iframe,因此不存在组件在文档范畴上的迁移,只能在同一个文档的不一样父节点之间迁移。因此也就不存在 adopted 状态,moved 函数能够理解为adopted 的一种变体;
小程序自定义组件独有的生命周期函数,created、ready 和 error;
Web Components 规范独有的生命周期函数,attributeChangedCallback。
可见小程序自定义组件与 Web Components 规范的主要差别体如今第 3 点和第 4 点。为何会有这样的差别呢?
差别点一:为何小程序的自定义组件没有attributeChangedCallback函数?
首先咱们要明确 attributeChangedCallback 函数的触发时机,Web Components 规范对这个函数的描述为“当自定义元素的任一属性发生改变(包括新增、删除、更新)时触发”。而更新元素属性这种行为是传统 DOM 编程中常见的,在目前倡导数据驱动 UI 的背景下,绝大多数框架都是经过 VDOM 来间接操做 DOM,因此更新属性在目前的时代背景下很是少见。
微信小程序与 Vue/React 同样,一样不容许直接操做 DOM,从根本上就不可能发生 DOM 属性改变的状况。这就解释了为什么小程序自定义组件的生命周期中没有 attributeChangedCallback 函数。
差别点二:Web Components 规范为什么没有 created/ready/error 三个函数?
技术规范是一种指导方针,具体的实现方式每每须要根据现实状况决定;Web Components 规范一样如此,它脱离于业务,单纯从技术的角度提供了最基础的标准和参考,具体到实现层面,Vue/React 之类的框架有各自的理解,微信小程序一样也有独到之处。
之因此有差别,一方面是出于各框架开发者对规范的理解和延伸,另外一方面是考虑到实际的业务须要,因此每每会有一些规范未覆盖的“创新”之处,最典型的就是 document.ready 事件。在DOMContentLoad 规范推出以前,jQuery 的 $(document).ready 事件已经在前端技术圈盛行了好久,这个事件发生了 window.onload 以前,此时的文档状态处于渲染未完成可是可交互,因此这个事件在优化网站性能的 FIT(First Load Time,提升加载速度)方面被频繁使用。
回到这个问题自己,小程序自定义组件的 created、ready 和 error 三个函数与 document.ready 有殊途同归之妙,都是结合框架自己特点以及业务需求所开发的超越标准规范以外的“创新”。
总的来讲,以上两个差别点的核心缘由能够归纳为一句话:理论上的规范在实现的时候须要结合现实的客观条件。规范是上层实现的参考标准,但并无限制和框定上层实现的具体模式。差别点一是因为小程序不存在操做 DOM 的状况,差别点二是因为created、ready 和 error 三个函数是超出规范以外、小程序根据自身技术特点的一种“创新”。 理解了自定义组件的资源管理和生命周期以后,你即可以开发出一个优秀的自定义组件了。可是正如上文提到的,一个 Page 中可能存在多个自定义组件,这些组件都是服务于同一个页面,不免会有一些数据上的流通。这时候就会遇到一个组件化领域很是典型的问题:各组件之间如何通讯?
// toast.wxml
<view class="container {{mask?'containerShowMask':'containerNoMask'}}" hidden="{{!status}}" style="z-index:{{zIndex}}">
<view class="loreal-bg-class toast-bg" wx:if="{{mask}}"></view>
<view class="loreal-class toast toast-{{placement || 'bottom'}}" style="padding-top:{{(placement || 'bottom')=== 'bottom' ? image || icon ? '25rpx': '': ''}};position:relative;left:{{offsetX}}rpx;top:{{offsetY}}rpx;margin-bottom:{{distance}}px"> <image class="loreal-image-class toast-icon" wx:if="{{image}}" src="{{image}}"/> <l-icon class="loreal-icon-class toast-icon toast-icon-{{icon === 'loading'?'loading':''}}" wx:elif="{{icon && !image}}" size="{{iconSize? iconSize : 60}}" color="{{iconColor? iconColor: icon === 'success'? '#00C292' : icon === 'error' ? '#F4516C' : '#ffffff'}}" name="{{icon}}"/> <slot wx:else/> <text class="toast-text loreal-title-class toast-text-{{placement}}" style="{{placement || 'bottom' === 'bottom' ? icon || image? 'margin-top:10rpx' : '': '' }}">{{ title }}</text> </view>
</view>
// toast.js
import validator from "../behaviors/validator";
Component({
externalClasses: ["loreal-class", "loreal-label-class", "loreal-hover-class", "loreal-img-class", "loreal-icon-class"],
behaviors: [validator],
properties: {
name: {
type: String,
value: "lin"
},
type: {
type: String,
value: "default",
options: ["warning", "success", "error", "default"]
},
plain: Boolean,
size: {
type: String,
value: "medium",
options: ["medium", "large", "mini", "long"]
},
shape: {
type: String,
value: "circle",
options: ["square", "circle", "semicircle"]
},
disabled: {
type: Boolean,
value: !1
},
special: {
type: Boolean,
value: !1
},
loading: {
type: Boolean,
value: !1
},
width: Number,
height: Number,
icon: String,
image: String,
bgColor: String,
iconColor: String,
iconSize: String,
openType: String,
appParameter: String,
lang: String,
hoverStopPropagation: Boolean,
hoverStartTime: {
type: Number,
value: 20
},
hoverStayTime: {
type: Number,
value: 70
},
sessionFrom: {
type: String,
value: ""
},
sendMessageTitle: String,
sendMessagePath: String,
sendMessageImg: String,
showMessageCard: Boolean,
formType: String
},
methods: {
handleTap() {
if (this.data.disabled || this.data.loading) return !1;
this.triggerEvent("lintap", {}, {
bubbles: !0,
composed: !0
})
},
openTypeEvent(e) {
this.triggerEvent(e.type, e.detail, {})
}
}
});
// toast.wxss
.container{position:fixed}.containerNoMask{left:50%;top:50%;transform:translate(-50%,-50%)}.containerShowMask{height:100%;width:100%;top:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:999}.container .toast-bg{height:100%;width:100%;background:rgba(255,255,255,.5);position:absolute;top:0;left:0}.container .toast-top{flex-direction:column-reverse}.container .toast-right{flex-direction:row}.container .toast-bottom{flex-direction:column}.container .toast-left{flex-direction:row-reverse}.container .toast{display:flex;align-items:center;justify-content:center;max-width:400rpx;min-width:280rpx;min-height:88rpx;background:rgba(0,0,0,.7);border-radius:12rpx;color:#fff;font-size:28rpx;line-height:40rpx;box-sizing:border-box;padding:30rpx 50rpx;z-index:999}.container .toast .toast-icon{margin-top:20rpx;margin-bottom:20rpx}.container .toast .toast-icon-loading{animation:loading-fadein 1.5s linear 0s infinite}.container .toast .toast-text{display:inline-block;text-align:center}.container .toast .toast-text-right{display:inline-block;text-align:center;margin-left:20rpx}.container .toast .toast-text-left{display:inline-block;text-align:center;margin-right:20rpx}.container .toast .toast-text-top{margin-bottom:10rpx}@keyframes loading-fadein{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
复制代码
除此以外,自定义组件还有一些特殊的生命周期,它们并不是与组件有很强的关联,但有时组件须要获知,以便组件内部处理。这样的生命周期称为“组件所在页面的生命周期”,在 pageLifetimes 定义段中定义。其中可用的生命周期包括:
生成的组件实例能够在组件的方法、生命周期函数和属性 observer 中经过 this 访问。组件包含一些通用属性和方法。
组件间交互的主要形式是自定义事件。组件经过 this.triggerEvent() 触发自定义事件,主页面在组件上 bind:myevent="onMyEvent" 来接收自定义事件。其中,this.triggerEvent() 方法接收自定义事件名称外,还接收两个对象,eventDetail 和 eventOptions。
<!-- 在自定义组件中 -->
<button bindtap="onTap">点击这个按钮将触发“myevent”事件</button>
Component({
properties: {}
methods: {
// 子组件触发自定义事件
ontap () {
// 全部要带到主页面的数据,都装在eventDetail里面
var eventDetail = {
name:'sssssssss',
test:[1,2,3]
}
// 触发事件的选项 bubbles是否冒泡,composed是否可穿越组件边界,capturePhase 是否有捕获阶段
var eventOption = {
composed: true
}
this.triggerEvent('myevent', eventDetail, eventOption)
}
}
})
复制代码
触发的事件包括:
自定义组件能够触发任意的事件,引用组件的页面能够监听这些事件。监听自定义组件事件的方法与监听基础组件事件的方法彻底一致:在Page事件中监听组件中传递过来的值。
Page({
onMyEvent: function(e){
e.detail // 自定义组件触发事件时提供的detail对象
}
})
复制代码
behaviors 是用于组件间代码共享的特性,相似于一些编程语言中的“mixins”或“traits”。每一个 behavior 能够包含一组属性、数据、生命周期函数和方法,组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。每一个组件能够引用多个 behavior 。behavior 也能够引用其余 behavior 。
// validator.js
module.exports = Behavior({
behaviors: [],
properties: {
myBehaviorProperty: {
type: String
}
},
data: {
myBehaviorData: {}
},
attached: function(){},
methods: {
myBehaviorMethod: function(){}
}
})
复制代码
组件引用时,在 behaviors 定义段中将它们逐个列出便可。
// my-component.js
var myBehavior = require('my-behavior')
Component({
behaviors: [myBehavior],
properties: {
myProperty: {
type: String
}
},
data: {
myData: {}
},
attached: function(){},
methods: {
myMethod: function(){}
}
})
复制代码
组件和它引用的 behavior 中能够包含同名的字段,对这些字段的处理方法以下:若是有同名的属性或方法,组件自己的属性或方法会覆盖 behavior 中的属性或方法,若是引用了多个 behavior ,在定义段中靠后 behavior 中的属性或方法会覆盖靠前的属性或方法;若是有同名的数据字段,若是数据是对象类型,会进行对象合并,若是是非对象类型则会进行相互覆盖;生命周期函数不会相互覆盖,而是在对应触发时机被逐个调用。若是同一个 behavior 被一个组件屡次引用,它定义的生命周期函数只会被执行一次。内置behavior 组件间关系
<custom-ul>
<custom-li> item 1 </custom-li>
<custom-li> item 2 </custom-li>
</custom-ul>
复制代码
这个例子中, custom-ul 和 custom-li 都是自定义组件,它们有相互间的关系,相互间的通讯每每比较复杂。此时在组件定义时加入 relations 定义段,能够解决这样的问题。示例:
// path/to/custom-ul.js
Component({
relations: {
'./custom-li': {
type: 'child', // 关联的目标节点应为子节点
linked: function(target) {
// 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期以后
},
linkChanged: function(target) {
// 每次有custom-li被移动后执行,target是该节点实例对象,触发在该节点moved生命周期以后
},
unlinked: function(target) {
// 每次有custom-li被移除时执行,target是该节点实例对象,触发在该节点detached生命周期以后
}
}
},
methods: {
_getAllLi: function(){
// 使用getRelationNodes能够得到nodes数组,包含全部已关联的custom-li,且是有序的
var nodes = this.getRelationNodes('path/to/custom-li')
}
},
ready: function(){
this._getAllLi()
}
})
// path/to/custom-li.js
Component({
relations: {
'./custom-ul': {
type: 'parent', // 关联的目标节点应为父节点
linked: function(target) {
// 每次被插入到custom-ul时执行,target是custom-ul节点实例对象,触发在attached生命周期以后
},
linkChanged: function(target) {
// 每次被移动后执行,target是custom-ul节点实例对象,触发在moved生命周期以后
},
unlinked: function(target) {
// 每次被移除时执行,target是custom-ul节点实例对象,触发在detached生命周期以后
}
}
}
})
复制代码
与 Vue/React 不一样,小程序没有相似 Vuex 或 Redux 数据流管理模块,因此小程序的自定义组件之间的通讯流程采用的是比较原始的事件驱动模式,即子组件经过抛出事件将数据传递给父组件,父组件经过 properties 将数据传递给子组件。
假设小程序的某个页面中存在两个组件,两个组件均依赖父组件(Page)的部分属性,这部分属性经过 properties 传递给子组件,以下图所示:
当组件 A 须要与组件 B 进行通讯时,会抛出一个事件通知父组件 Page,父组件接收到事件以后提取事件携带的信息,而后经过 properties 传递给组件 B。这样便完成了子组件之间的消息传递。
除了事件驱动的通讯方式之外,小程序还提供了一种更加简单粗暴的方法:父组件经过selectComponent 方法直接获取某个子组件的实例对象,而后就能够访问这个子组件的任何属性和方法了。随后将这个子组件的某个属性经过 properties传递个另一个子组件。相较而言,事件驱动的方法更加优雅,在流程上也更加可控,因此一般建议使用事件驱动的通讯方式。