韶關微信小程序中怎么自定義組件?下面本篇文章給大家介紹一下微信小程序中自定義組件的方法,希望對大家有所幫助!
在微信小程序開發過程中,對于一些可能在多個頁面都使用的頁面模塊,可以把它封裝成一個組件,以提高開發效率。雖然說我們可以引入整個組件庫比如 weui、vant 等,但有時候考慮微信小程序的包體積限制問題,通常封裝為自定義的組件更為可控。
并且對于一些業務模塊,我們就可以封裝為組件復用。本文主要講述以下兩個方面:
組件的聲明與使用
微信小程序的組件系統底層是通過 Exparser 組件框架實現,它內置在小程序的基礎庫中,小程序內的所有組件,包括內置組件和自定義組件都由 Exparser 組織管理。
自定義組件和寫頁面一樣包含以下幾種文件:
- index.json
- index.wxml
- index.wxss
- index.js
- index.wxs
以編寫一個 tab
組件為例: 編寫自定義組件時需要在 json
文件中講 component
字段設為 true
:
在 js
文件中,基礎庫提供有 Page 和 Component 兩個構造器,Page 對應的頁面為頁面根組件,Component 則對應:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | Component({
options: {
addGlobalClass: true ,
pureDataPattern: /^_/,
multipleSlots: true
},
properties: {
vtabs: {type: Array, value: []},
},
data: {
currentView: 0,
},
observers: {
activeTab: function (activeTab) {
this .scrollTabBar(activeTab);
}
},
relations: {
'../vtabs-content/index' : {
type: 'child' ,
linked: function (target) {
this .calcVtabsCotentHeight(target);
},
unlinked: function (target) {
delete this .data._contentHeight[target.data.tabIndex];
}
}
},
lifetimes: {
created: function () {
},
attached: function () {
},
detached: function () {
},
},
methods: {
calcVtabsCotentHeight(target) {}
}
});
|
如果有了解過 Vue2 的小伙伴,會發現這個聲明很熟悉。
在小程序啟動時,構造器會將開發者設置的properties、data、methods等定義段,
寫入Exparser的組件注冊表中。這個組件在被其它組件引用時,就可以根據這些注冊信息來創建自定義組件的實例。
模版文件 wxml:
1 2 3 | < view class = 'vtabs' >
< slot />
</ view >
|
樣式文件:
外部頁面組件使用,只需要在頁面的 json
文件中引入
1 2 3 4 5 6 | {
"navigationBarTitleText" : "商品分類" ,
"usingComponents" : {
"vtabs" : "../../../components/vtabs" ,
}
}
|
在初始化頁面時,Exparser 會創建出頁面根組件的一個實例,用到的其他組件也會響應創建組件實例(這是一個遞歸的過程):
組件創建的過程大致有以下幾個要點:
根據組件注冊信息,從組件原型上創建出組件節點的 JS
對象,即組件的 this
;
將組件注冊信息中的 data
復制一份,作為組件數據,即 this.data
;
將這份數據結合組件 WXML
,據此創建出 Shadow Tree
(組件的節點樹),由于 Shadow Tree
中可能引用有其他組件,因而這會遞歸觸發其他組件創建過程;
將 ShadowTree
拼接到 Composed Tree
(最終拼接成的頁面節點樹)上,并生成一些緩存數據用于優化組件更新性能;
觸發組件的 created
生命周期函數;
如果不是頁面根組件,需要根據組件節點上的屬性定義,來設置組件的屬性值;
當組件實例被展示在頁面上時,觸發組件的 attached
生命周期函數,如果 Shadow Tree
中有其他組件,也逐個觸發它們的生命周期函數。
組件通信
由于業務的負責度,我們常常需要把一個大型頁面拆分為多個組件,多個組件之間需要進行數據通信。
對于跨代組件通信可以考慮全局狀態管理,這里只討論常見的父子組件通信:
方法一 WXML 數據綁定
用于父組件向子組件的指定屬性設置數據。
子聲明 properties 屬性
1 2 3 4 5 | Component({
properties: {
vtabs: {type: Array, value: []},
}
})
|
父組件調用:
1 | < vtabs vtabs = "{{ vtabs }}" </vtabs>
|
方法二 事件
用于子組件向父組件傳遞數據,可以傳遞任意數據。
子組件派發事件,先在 wxml 結構綁定子組件的點擊事件:
1 | < view bindtap = "handleTabClick" >
|
再在 js 文件中進行派發事件,事件名可以自定義填寫, 第二個參數可以傳遞數據對象,第三個參數為事件選項。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | handleClick(e) {
this .triggerEvent(
'tabclick' ,
{ index },
{
bubbles: false ,
composed: false ,
capturePhase: false
}
);
},
handleChange(e) {
this .triggerEvent( 'tabchange' , { index });
},
|
最后,在父組件中監聽使用:
1 2 3 4 5 | <vtabs
vtabs= "{{ vtabs }}"
bindtabclick= "handleTabClick"
bindtabchange= "handleTabChange"
>
|
方法三 selectComponent 獲取組件實例對象
通過 selectComponent
方法可以獲取子組件的實例,從而調用子組件的方法。
父組件的 wxml
1 2 3 | <view>
<vtabs-content= "goods-content{{ index }}" ></vtabs-content>
</view>
|
父組件的 js
1 2 3 4 5 | Page({
reCalcContentHeight(index) {
const goodsContent = this .selectComponent(` #goods-content${index}`);
},
})
|
selector類似于 CSS 的選擇器,但僅支持下列語法。
- ID選擇器:
#the-id
(筆者只測試了這個,其他讀者可自行測試) - class選擇器(可以連續指定多個):
.a-class.another-class
- 子元素選擇器:
.the-parent > .the-child
- 后代選擇器:
.the-ancestor .the-descendant
- 跨自定義組件的后代選擇器:
.the-ancestor >>> .the-descendant
- 多選擇器的并集:
#a-node
, .some-other-nodes
方法四 url 參數通信
在電商/物流等微信小程序中,會存在這樣的用戶故事,有一個「下單頁面A」和「貨物信息頁面B」
- 在「下單頁面 A」填寫基本信息,需要下鉆到「詳細頁面B」填寫詳細信息的情況。比如一個寄快遞下單頁面,需要下鉆到貨物信息頁面填寫更詳細的信息,然后返回上一個頁面。
- 在「下單頁面 A」下鉆到「貨物頁面B」,需要回顯「貨物頁面B」的數據。
微信小程序由一個 App()
實例和多個 Page()
組成。小程序框架以棧的方式維護頁面(最多10個) 提供了以下 API 進行頁面跳轉,頁面路由如下
wx.navigateTo(只能跳轉位于棧內的頁面)
wx.redirectTo(可跳轉位于棧外的新頁面,并替代當前頁面)
wx.navigateBack(返回上一層頁面,不能攜帶參數)
wx.switchTab(切換 Tab 頁面,不支持 url 參數)
wx.reLaunch(小程序重啟)
可以簡單封裝一個 jumpTo 跳轉函數,并傳遞參數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | export function jumpTo(url, options) {
const baseUrl = url.split( '?' )[0];
if (url.indexof( '?' ) !== -1) {
const { queries } = resolveUrl(url);
Object.assign(options, queries, options);
}
cosnt queryString = objectEntries(options)
.filter(item => item[1] || item[0] === 0)
.map(
([key, value]) => {
if ( typeof value === 'object' ) {
value = JSON.stringify(value);
}
if ( typeof value === 'string' ) {
value = encodeURIComponent(value);
}
return `${key}=${value}`;
}
).join( '&' );
if (queryString) {
url = `${baseUrl}?${queryString}`;
}
const pageCount = wx.getCurrentPages().length;
if (jumpType === 'navigateTo' && pageCount < 5) {
wx.navigateTo({
url,
fail: () => {
wx. switch ({ url: baseUrl });
}
});
} else {
wx.navigateTo({
url,
fail: () => {
wx. switch ({ url: baseUrl });
}
});
}
}
|
jumpTo 輔助函數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | export const resolveSearch = search => {
const queries = {};
cosnt paramList = search.split( '&' );
paramList.forEach(param => {
const [key, value = '' ] = param.split( '=' );
queries[key] = value;
});
return queries;
};
export const resolveUrl = (url) => {
if (url.indexOf( '?' ) === -1) {
return {
queries: {},
page: url
}
}
const [page, search] = url.split( '?' );
const queries = resolveSearch(search);
return {
page,
queries
};
};
|
在「下單頁面A」傳遞數據:
1 2 3 4 5 6 | jumpTo({
url: 'pages/consignment/index' ,
{
sender: { name: 'naluduo233' }
}
});
|
在「貨物信息頁面B」獲得 URL 參數:
1 | const sender = JSON.parse(getParam( 'sender' ) || '{}' );
|
url 參數獲取輔助函數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | export function getCurrentPage() {
const pageStack = wx.getCurrentPages();
const lastIndex = pageStack.length - 1;
const currentPage = pageStack[lastIndex];
return currentPage;
}
export function getParams() {
const currentPage = getCurrentPage() || {};
const allParams = {};
const { route, options } = currentPage;
if (options) {
const entries = objectEntries(options);
entries.forEach(
([key, value]) => {
allParams[key] = decodeURIComponent(value);
}
);
}
return allParams;
}
export function getParam(name) {
const params = getParams() || {};
return params[name];
}
|
參數過長怎么辦?路由 api 不支持攜帶參數呢?
雖然微信小程序官方文檔沒有說明可以頁面攜帶的參數有多長,但還是可能會有參數過長被截斷的風險。
我們可以使用全局數據記錄參數值,同時解決 url 參數過長和路由 api 不支持攜帶參數的問題。
1 2 3 4 5 6 7 | const queryMap = {
page: '' ,
queries: {}
};
|
更新跳轉函數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | export function jumpTo(url, options) {
Object.assign(queryMap, {
page: baseUrl,
queries: options
});
if (jumpType === 'switchTab' ) {
wx.switchTab({ url: baseUrl });
} else if (jumpType === 'navigateTo' && pageCount < 5) {
wx.navigateTo({
url,
fail: () => {
wx. switch ({ url: baseUrl });
}
});
} else {
wx.navigateTo({
url,
fail: () => {
wx. switch ({ url: baseUrl });
}
});
}
}
|
url 參數獲取輔助函數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | export function getParams() {
const currentPage = getCurrentPage() || {};
const allParams = {};
const { route, options } = currentPage;
if (options) {
const entries = objectEntries(options);
entries.forEach(
([key, value]) => {
allParams[key] = decodeURIComponent(value);
}
);
+ if (isTabBar(route)) {
+
+ const { page, queries } = queryMap;
+ if (page === `${route}`) {
+ Object.assign(allParams, queries);
+ }
+ }
}
return allParams;
}
|
輔助函數
1 2 3 | const { tabBar} = appConfig;
export isTabBar = (route) => tabBar.list.some(({ pagePath })) => pagePath === route);
|
按照這樣的邏輯的話,是不是都不用區分是否是 isTabBar
頁面了,全部頁面都從 queryMap 中獲???這個問題目前后續探究再下結論,因為我目前還沒試過從頁面實例的 options
中拿到的值是缺少的。所以可以先保留讀取 getCurrentPages
的值。
方法五 EventChannel 事件派發通信
前面我談到從「當前頁面A」傳遞數據到被打開的「頁面B」可以通過 url 參數。那么想獲取被打開頁面傳送到當前頁面的數據要如何做呢?是否也可以通過 url 參數呢?
答案是可以的,前提是不需要保存「頁面A」的狀態。如果要保留「頁面 A」的狀態,就需要使用 navigateBack
返回上一頁,而這個 api 是不支持攜帶 url 參數的。
這樣時候可以使用 頁面間事件通信通道 EventChannel。
pageA 頁面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | wx.navigateTo({
url: 'pageB?id=1' ,
events: {
acceptDataFromOpenedPage: function (data) {
console.log(data)
},
},
success: function (res) {
res.eventChannel.emit( 'acceptDataFromOpenerPage' , { data: 'test' })
}
});
|
pageB 頁面
1 2 3 4 5 6 7 8 9 10 11 | Page({
onLoad: function (option){
const eventChannel = this .getOpenerEventChannel()
eventChannel.emit( 'acceptDataFromOpenedPage' , {data: 'test' });
eventChannel.on( 'acceptDataFromOpenerPage' , function (data) {
console.log(data)
})
}
})
|
會出現數據無法監聽的情況嗎?
小程序的棧不超過 10 層,如果當前「頁面A」不是第 10 層,那么可以使用 navigateTo
跳轉保留當前頁面,跳轉到「頁面B」,這個時候「頁面B」填寫完畢后傳遞數據給「頁面A」時,「頁面A」是可以監聽到數據的。
如果當前「頁面A」已經是第10個頁面,只能使用 redirectTo
跳轉「PageB」頁面。結果是當前「頁面A」出棧,新「頁面B」入棧。這個時候將「頁面B」傳遞數據給「頁面A」,調用 navigateBack
是無法回到目標「頁面A」的,因此數據是無法正常被監聽到。
不過我分析做過的小程序中,棧中很少有10層的情況,5 層的也很少。因為調用 wx.navigateBack
、wx.redirectTo
會關閉當前頁面,調用 wx.switchTab
會關閉其他所有非 tabBar 頁面。
所以很少會出現這樣無法回到上一頁面以監聽到數據的情況,如果真出現這種情況,首先要考慮的不是數據的監聽問題了,而是要保證如何能夠返回上一頁面。
比如在「PageA」頁面中先調用 getCurrentPages
獲取頁面的數量,再把其他的頁面刪除,之后在跳轉「PageB」頁面,這樣就避免「PageA」調用 wx.redirectTo
導致關閉「PageA」。但是官方是不推薦開發者手動更改頁面棧的,需要慎重。
如果有讀者遇到這種情況,并知道如何解決這種的話,麻煩告知下,感謝。
使用自定義的事件中心 EventBus
除了使用官方提供的 EventChannel 外,我們也可以自定義一個全局的 EventBus 事件中心。 因為這樣更加靈活,不需要在調用 wx.navigateTo
等APi里傳入參數,多平臺的遷移性更強。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | export default class EventBus {
private defineEvent = {};
public register(event: string, cb): void {
if (! this .defineEvent[event]) {
( this .defineEvent[event] = [cb]);
}
else {
this .defineEvent[event].push(cb);
}
}
public dispatch(event: string, arg?: any): void {
if ( this .defineEvent[event]) {{
for (let i=0, len = this .defineEvent[event].length; i<len; ++i) {
this .defineEvent[event][i] && this .defineEvent[event][i](arg);
}
}}
}
public on(event: string, cb): void {
return this .register(event, cb);
}
public off(event: string, cb?): void {
if ( this .defineEvent[event]) {
if ( typeof (cb) == "undefined" ) {
delete this .defineEvent[event];
} else {
for (let i=0, len= this .defineEvent[event].length; i<len; ++i) {
|