微信小程式元件化的解決方案

霧豹發表於2019-01-16

從小程式基礎庫版本 1.6.3 開始,小程式支援簡潔的元件化程式設計。檢視自己使用的小程式基礎庫版本,可以通過在開發者工具右側點選詳情檢視

最基本的元件

小程式的元件,其實就是一個目錄,該目錄需要包含4個檔案:

  1. xxx.json
  2. xxx.wxml
  3. xxx.wxss
  4. 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() 方法接收自定義事件名稱外,還接收兩個物件,eventDetaileventOptions

// 子元件觸發自定義事件
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"
    }
}
複製程式碼
  1. relations 裡面的路徑,比如說這裡:

img

是對方元件真實的相對路徑,而不是元件間的邏輯路徑。

  1. 如果relations 沒有關聯,那麼 this.getRelationNodes 是獲取不到對方元件的

  2. 本元件無法獲取本元件的例項,使用this.getRelatonsNodes('./ path_to_self ') 會返回一個null

  3. type 可以選擇的 parentchildancestordescendant

現在我們已經可以做到了兩個元件之間的資料傳遞,那麼如何在多個元件間傳遞資料呢?

img

如上圖所示,同級的元件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 像一個隱形的鏈子一樣把一堆元件關聯起來,關聯起來的元件可以相互訪問,修改對方的資料,但是每一個元件都可以從外界獨立的獲取資料。

看了這麼多理論的東西,還是需要一個具體的場景來應用。

比如說,我們有個一個分享記錄圖片心情的頁面,當使用者點選【點贊】的按鈕時候,該心情的記錄 點贊按鈕會變紅,下面的一欄位置會多出點贊人的名字。

img

如果不通過元件化,很可能的做法是 修改一個點贊按鈕,然後遍歷資料更新資料,最後所有記錄列表的狀態都會被重新渲染一遍。

如果是通過元件化拆分:把點讚的按鈕封裝為 元件b, 下面點贊人的框封裝為元件c, 每一個心情記錄都是一個元件a

img

下面是程式碼實現

// 在主頁面內
<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 和 事件系統,和主頁面保持獨立的資料通訊。

相關文章