從小程式基礎庫版本 1.6.3 開始,小程式支援簡潔的元件化程式設計。檢視自己使用的小程式基礎庫版本,可以通過在開發者工具右側點選詳情檢視
最基本的元件
小程式的元件,其實就是一個目錄,該目錄需要包含4個檔案:
- xxx.json
- xxx.wxml
- xxx.wxss
- xxx.js
宣告一個元件
首先需要在 json
檔案中進行自定義元件宣告(將 component
欄位設為 true
可這一組檔案設為自定義元件)
{ "component": true}
複製程式碼
其次,在要引入元件的頁面的json檔案內,進行引用宣告
{
"usingComponents": {
"component-tag-name": "path/to/the/custom/component"
}
}
複製程式碼
component-tag-name
欄位是自定義的元件名稱
後面的是元件路徑,注意是相對路徑,不能是絕對路徑
這樣,在主頁面就可以使用了。
相比於vue的元件引入,小程式的方案更簡潔。vue元件引入是需要 import 之後,同時在 components 裡面註冊,而小程式的元件只需要在 .json 裡面註冊,就可以在 wxml 裡面使用。
使用slot
和vue 相同,小程式也有slot概念。
單一slot
在元件模板中可以提供一個 <slot>
節點,用於承載元件引用時提供的子節點。
// 主頁面內,<addlike>是元件
<addlike item="item" my_properties="sssss">
<text>我是被slot插入的文字</text>
</addlike>
// addlike 元件
<view class="container">
<view>hello, 這裡是元件</view>
<view>hello, {{my_properties}}</view>
<slot></slot>
</view>
// 渲染後
<view class="container">
<view>hello, 這裡是元件</view>
<view>hello, {{my_properties}}</view>
<text>我是被slot插入的文字</text>
</view>
複製程式碼
多個slot
如果需要在元件內使用多個slot, 需要在元件js中宣告啟用:
Component({
options: {
multipleSlots: true // 在元件定義時的選項中啟用多slot支援
},
properties: { /* ... */ },
methods: { /* ... */ }
})
複製程式碼
使用:
// 主頁面
<addlike item="item" my_properties="sssss">
// 在普通的元素上加入 slot 屬性,指定slotname, 就可以變成子元素的slot了
<text slot="slot1">我是被slot1插入的文字</text>
<text slot="slot2">我是被slot2插入的文字</text>
</addlike>
// 子頁面
<view class="container">
<view>hello, 這裡是元件</view>
<view>hello, {{my_properties}}</view>
<slot name="slot1"></slot>
<slot name="slot2"></slot>
</view>
複製程式碼
Component構造器
剛才我們說了,一個元件內應該包括js, wxml, wxss, json 四個檔案。wxml 相當於是 HTML,wxss 相當於是 css, 那麼js 裡面應該寫什麼呢?
微信官方提供的案例中:
Component({
behaviors: [],
properties: {
},
data: {}, // 私有資料,可用於模版渲染
// 生命週期函式,可以為函式,或一個在methods段中定義的方法名
attached: function(){},
moved: function(){},
detached: function(){},
methods: {
onMyButtonTap: function(){
},
_myPrivateMethod: function(){
},
_propertyChange: function(newVal, oldVal) {
}
}
})
複製程式碼
裡面呼叫了一個Component構造器。Component構造器可用於定義元件,呼叫Component構造器時可以指定元件的屬性、資料、方法等。具體 Component裡面可以放什麼東西,如下所示:
properties | Object Map | 否 | 相當於是vue的props,通過該屬性,外界向元件內傳入資料。元件的對外屬性,是屬性名到屬性設定的對映表,屬性設定中可包含三個欄位, type 表示屬性型別、 value 表示屬性初始值、 observer 表示屬性值被更改時的響應函式 |
---|---|---|---|
data | Object | 否 | 元件的內部資料,和 properties 一同用於元件的模版渲染。也就是說,通過this.data 可以同時獲得 data 和 properties |
methods | Object | 否 | 元件的方法,包括事件響應函式和任意的自定義方法,關於事件響應函式的使用,參見 元件事件 |
behaviors | String Array | 否 | 類似於mixins和traits的元件間程式碼複用機制,參見 behaviors |
created | Function | 否 | 元件生命週期函式,在元件例項進入頁面節點樹時執行,注意此時不能呼叫 setData |
attached | Function | 否 | 元件生命週期函式,在元件例項進入頁面節點樹時執行 |
ready | Function | 否 | 元件生命週期函式,在元件佈局完成後執行,此時可以獲取節點資訊(使用 SelectorQuery ) |
moved | Function | 否 | 元件生命週期函式,在元件例項被移動到節點樹另一個位置時執行 |
detached | Function | 否 | 元件生命週期函式,在元件例項被從頁面節點樹移除時執行 |
relations | Object | 否 | 元件間關係定義,參見 元件間關係 |
options | Object Map | 否 | 一些元件選項,請參見文件其他部分的說明 |
元件與資料通訊
元件化必然要涉及到資料的通訊,為了解決資料在元件間的維護問題,vue, react,angular 有不同的解決方案。而小程式的解決方案則簡潔很多。
主頁面傳入資料到元件
properties相當於vue的props,是傳入外部資料的入口。
// 主頁面使用元件
<a add_like="{{add_like}}">
</a>
// 元件a.js 內
Component({
properties:{
add_like:{
type:Array,
value:[],
observer:function(){
}
}
}
})
複製程式碼
注意: 傳入的資料,不管是簡單資料型別,還是引用型別,都如同值複製一樣(和紅寶書裡面描述js函式引數傳入是值複製還不一樣,紅寶書裡面的意思是:簡單資料型別直接複製數值,引用型別複製引用,也就是說在函式內修改引數物件的屬性,會影響到函式外物件的屬性)。
如果是Vue的props, 則可以通過.sync
來同步,而在小程式子元件裡面,呼叫this.setData()修改父元件內的資料,不會影響到父元件裡面的資料, 也就是說,子元件property
的修改,彷彿和父元件沒有任何關係。那麼,如果是在子元件內修改父元件的資料,甚至是修改兄弟元件內的資料,有沒有簡單的方法呢?下面會有講到
元件傳出資料到主頁面
和vue類似,元件間互動的主要形式是自定義事件。
元件通過this.triggerEvent()
觸發自定義事件,主頁面在元件上 bind:component_method="main_page_mehod"
來接收自定義事件。
其中,this.triggerEvent()
方法接收自定義事件名稱外,還接收兩個物件,eventDetail
和 eventOptions
。
// 子元件觸發自定義事件
ontap () {
// 所有要帶到主頁面的資料,都裝在eventDetail裡面
var eventDetail = {
name:'sssssssss',
test:[1,2,3]
}
// 觸發事件的選項 bubbles是否冒泡,composed是否可穿越元件邊界,capturePhase 是否有捕獲階段
var eventOption = {
composed: true
}
this.triggerEvent('click_btn', eventDetail, eventOption)
}
// 主頁面裡面
main_page_ontap (eventDetail) {
console.log(eventDetail)
// eventDetail
// changedTouches
// currentTarget
// target
// type
// ……
// detail 哈哈,所有的子元件的資料,都通過該引數的detail屬性暴露出來
}
複製程式碼
元件之間資料通訊
和vue提出的vuex的解決方案不同,小程式的元件間的通訊簡單小巧。你可以和主頁面與元件通訊一樣,使用自定義事件來進行通訊,當然更簡單方便的方法,是使用小程式提供的relations.
relations 是Component 建構函式中的一個屬性,只要兩個元件的relations 屬性產生關聯,他們兩個之間就可以捕獲到對方,並且可以相互訪問,修改對方的屬性,如同修改自己的屬性一樣。
Component({
relations:{
'./path_to_b': { // './path_to_b'是對方元件的相對路徑
type: 'child', // type可選擇兩組:parent和child、ancestor和descendant
linked:function(target){ } // 鉤子函式,在元件linked時候被呼叫 target是元件的例項,
linkChanged: function(target){}
unlinked: function(target){}
}
},
})
複製程式碼
比如說,有兩個元件如程式碼所示:
// 元件a slot 包含了元件b
<a>
<b></b>
</a>
複製程式碼
他們之間的關係如下圖所示:
兩個元件捕獲到對方元件的例項,是通過 this.getRelationNodes('./path_to_a')方法。既然獲取到了對方元件的例項,那麼就可以訪問到對方元件上的data, 也可以設定對方元件上的data, 但是不能呼叫對方元件上的方法。
// 在a 元件中
Component({
relations:{
'./path_to_b': {
type: 'child',
linked:function(target){ } // target是元件b的例項,
linkChanged: function(target){}
unlinked: function(target){}
}
},
methods:{
test () {
var nodes = this.getRelationNodes('./path_to_b')
var component_b = nodes[0];
// 獲取到b元件的資料
console.log(component_b.data.name)
// 設定父元件的資料
// 這樣的設定是無效的
this.setData({
component_b.data.name:'ss'
})
// 需要呼叫對方元件的setData()方法來設定
component_b.setData({
name:'ss'
})
}
}
})
// 在b 元件裡面
Component({
relations:{
'./path_to_a': { //注意!必須雙方元件都宣告relations屬性
type:'parent'
}
},
data: {
name: 'dudu'
}
})
複製程式碼
注意:1. 主頁面使用元件的時候,不能有數字,比如說 <component_sub1> 或 <component_sub_1>,可以在主頁面的json 裡面設定一個新名字
{
"usingComponents":{
"test_component_subb": "../../../components/test_component_sub2/test_component_sub2"
}
}
複製程式碼
- relations 裡面的路徑,比如說這裡:
是對方元件真實的相對路徑,而不是元件間的邏輯路徑。
-
如果relations 沒有關聯,那麼 this.getRelationNodes 是獲取不到對方元件的
-
本元件無法獲取本元件的例項,使用this.getRelatonsNodes('./ path_to_self ') 會返回一個null
-
type 可以選擇的
parent
、child
、ancestor
、descendant
現在我們已經可以做到了兩個元件之間的資料傳遞,那麼如何在多個元件間傳遞資料呢?
如上圖所示,同級的元件b 和同級的元件c , b 和 c 之間不可以直接獲取,b可以獲取到a, c 也可以獲取到a,而a可以直接獲取到 b 和 c。所以,如果想獲取到兄弟元素,需要先獲取到祖先節點,然後再通過祖先節點獲取兄弟節點
我在
元件b
裡面,我需要先找到祖先元件a
的例項,然後用祖先元件a
的例項的getRelationNodes
方法獲取到元件c
的例項。
看見沒?恐怕我們又要寫一大堆重複性的程式碼了。
幸好,微信小程式還提供了behavior 屬性, 這個屬性相當於 mixin,很容易理解的,是提高程式碼複用性的一種方法。
思路:
假設目前有三個元件,元件a, 元件b, 元件c, 其中元件b和元件c是兄弟元件,組建a是b和c的兄弟元件。為了減少程式碼的重複性,我們把獲取父元件的方法,和獲取兄弟元件的方法封裝一下,封裝在 behavior 的 methods 中。只要是引入該behavior的元件,都可以便捷的呼叫方法。
實現:
新建一個behavior檔案,命名無所謂,比如說relation_behavior.js
// 在 get_relation.js 檔案裡面
module.exports = Behavior({
methods:{
// 獲取父元件例項的快捷方法
_parent () {
// 如果根據該路徑獲取到acestor元件為null,則說明this為ancesor
var parentNode = this.getRelationNodes('../record_item/record_item')
if (parentNode&&parentNode.length !== 0) {
return parentNode[0]
} else {
return this
}
},
// 獲取兄弟元件例項的快捷方法
_sibling(name) {
var node = this._parent().getRelationNodes(`../${name}/${name}`)
if (node &&node.length > 0) {
return node[0]
}
}
}
})
複製程式碼
然後在元件b, 和 元件c 上引入該behavior,並且呼叫方法,獲取父元件和兄弟元件的例項
// 元件b中
var relation_behavior = require('./path_to_relation_behavior')
Component({
behaviors:[relation_behavior],
methods:{
test () {
// 獲得父元件的例項
let parent = this._parent()
// 訪問父元件的資料d
console.log(parent.data.name)
// 修改父元件的資料
parent.setData({
name: 'test1'
})
// 獲得兄弟元件的例項
let sibling = this._sibling('c')
// 訪問兄弟元件的資料
console.log(sibling.data.name)
// 修改兄弟元件的資料
sibling.setData({
name:"test"
})
}
}
})
// 元件c中
var relation_behavior = require('./path_to_relation_behavior')
Component({
behaviors:[relation_behavior],
methods:{
test () {
// 獲得父元件的例項
let parent = this._parent()
// 訪問父元件的資料d
console.log(parent.data.name)
// 修改父元件的資料
parent.setData({
name: 'test1'
})
// 獲得兄弟元件的例項
let sibling = this._sibling('b')
// 訪問兄弟元件的資料
console.log(sibling.data.name)
// 修改兄弟元件的資料
sibling.setData({
name:"test"
})
}
}
})
複製程式碼
同時需要注意,c和b兩個元件,從relations屬性的角度來說,是a的後代元件。
但是元件b和元件c 所處的作用域, 都是主頁面的作用域,傳入的property都是主頁面的property,這樣就保證了元件資料的靈活性。relations 像一個隱形的鏈子一樣把一堆元件關聯起來,關聯起來的元件可以相互訪問,修改對方的資料,但是每一個元件都可以從外界獨立的獲取資料。
看了這麼多理論的東西,還是需要一個具體的場景來應用。
比如說,我們有個一個分享記錄圖片心情的頁面,當使用者點選【點贊】的按鈕時候,該心情的記錄 點贊按鈕會變紅,下面的一欄位置會多出點贊人的名字。
如果不通過元件化,很可能的做法是 修改一個點贊按鈕,然後遍歷資料更新資料,最後所有記錄列表的狀態都會被重新渲染一遍。
如果是通過元件化拆分:把點讚的按鈕封裝為 元件b
, 下面點贊人的框封裝為元件c,
每一個心情記錄都是一個元件a
下面是程式碼實現
// 在主頁面內
<view wx:for='{{feed_item}}'>
<a item='{{item}}'>
<b></b>
<c></c>
</a>
<view>
// 在元件a內
var behavior_relation = require('../../relation_behavior.js) //這裡引入上文說的Behavior
Component({
behaviors:[behavior_relation],
relations:{
'../b/b':{
type: 'descendant'
}
}
})
// 在元件b內
var behavior_relation = require('../../relation_behavior.js) //這裡引入上文說的Behavior
Component({
behaviors:[behavior_relation]
relations:{
'../a/a':{
type: 'ancestor'
}
},
data: {
is_like: false //控制點贊圖示的狀態
},
methods:{
// 當使用者點讚的時候
onClick () {
// 修改本元件的狀態
this.setData({
is_like: true
})
// 修改 c 元件的資料
this._sibling('c').setData({
likeStr: this._sibling('c').data.likeStr + '我'
})
}
}
})
// 在元件c內
var behavior_relation = require('../../relation_behavior.js) //這裡引入上文說的Behavior
Component({
behaviors:[behavior_relation],
relations:{
'../a/a':{
type: 'ancestor'
}
},
data:{
likeStr:'曉紅,小明'
}
})
複製程式碼
這樣,元件b 可以修改元件c中的資料。同時,元件b 和 元件c 又可以通過 properties 和 事件系統,和主頁面保持獨立的資料通訊。