微信小程序中怎么自定義組件?下面本篇文章給大家介紹一下微信小程序中自定義組件的方法,希望對(duì)大家有所幫助!

在微信小程序開發(fā)過程中,對(duì)于一些可能在多個(gè)頁面都使用的頁面模塊,可以把它封裝成一個(gè)組件,以提高開發(fā)效率。雖然說我們可以引入整個(gè)組件庫比如 weui、vant 等,但有時(shí)候考慮微信小程序的包體積限制問題,通常封裝為自定義的組件更為可控。
并且對(duì)于一些業(yè)務(wù)模塊,我們就可以封裝為組件復(fù)用。本文主要講述以下兩個(gè)方面:
組件的聲明與使用
組件通信
組件的聲明與使用
微信小程序的組件系統(tǒng)底層是通過 Exparser 組件框架實(shí)現(xiàn),它內(nèi)置在小程序的基礎(chǔ)庫中,小程序內(nèi)的所有組件,包括內(nèi)置組件和自定義組件都由 Exparser 組織管理。
自定義組件和寫頁面一樣包含以下幾種文件:
index.json
index.wxml
index.wxss
index.js
index.wxs
以編寫一個(gè) tab 組件為例: 編寫自定義組件時(shí)需要在 json 文件中講 component 字段設(shè)為 true:
{
"component": true
}在 js 文件中,基礎(chǔ)庫提供有 Page 和 Component 兩個(gè)構(gòu)造器,Page 對(duì)應(yīng)的頁面為頁面根組件,Component 則對(duì)應(yīng):
Component({
options: { // 組件配置
addGlobalClass: true,
// 指定所有 _ 開頭的數(shù)據(jù)字段為純數(shù)據(jù)字段
// 純數(shù)據(jù)字段是一些不用于界面渲染的 data 字段,可以用于提升頁面更新性能
pureDataPattern: /^_/,
multipleSlots: true // 在組件定義時(shí)的選項(xiàng)中啟用多slot支持
},
properties: {
vtabs: {type: Array, value: []},
},
data: {
currentView: 0,
},
observers: { // 監(jiān)測(cè)
activeTab: function(activeTab) {
this.scrollTabBar(activeTab);
}
},
relations: { // 關(guān)聯(lián)的子/父組件
'../vtabs-content/index': {
type: 'child', // 關(guān)聯(lián)的目標(biāo)節(jié)點(diǎn)應(yīng)為子節(jié)點(diǎn)
linked: function(target) {
this.calcVtabsCotentHeight(target);
},
unlinked: function(target) {
delete this.data._contentHeight[target.data.tabIndex];
}
}
},
lifetimes: { // 組件聲明周期
created: function() {
// 組件實(shí)例剛剛被創(chuàng)建好時(shí)
},
attached: function() {
// 在組件實(shí)例進(jìn)入頁面節(jié)點(diǎn)樹時(shí)執(zhí)行
},
detached: function() {
// 在組件實(shí)例被從頁面節(jié)點(diǎn)樹移除時(shí)執(zhí)行
},
},
methods: { // 組件方法
calcVtabsCotentHeight(target) {}
}
});如果有了解過 Vue2 的小伙伴,會(huì)發(fā)現(xiàn)這個(gè)聲明很熟悉。
在小程序啟動(dòng)時(shí),構(gòu)造器會(huì)將開發(fā)者設(shè)置的properties、data、methods等定義段,
寫入Exparser的組件注冊(cè)表中。這個(gè)組件在被其它組件引用時(shí),就可以根據(jù)這些注冊(cè)信息來創(chuàng)建自定義組件的實(shí)例。
模版文件 wxml:
<view class='vtabs'> <slot /> </view>
樣式文件:
.vtabs {}外部頁面組件使用,只需要在頁面的 json 文件中引入
{
"navigationBarTitleText": "商品分類",
"usingComponents": {
"vtabs": "../../../components/vtabs",
}
}在初始化頁面時(shí),Exparser 會(huì)創(chuàng)建出頁面根組件的一個(gè)實(shí)例,用到的其他組件也會(huì)響應(yīng)創(chuàng)建組件實(shí)例(這是一個(gè)遞歸的過程):
組件創(chuàng)建的過程大致有以下幾個(gè)要點(diǎn):
根據(jù)組件注冊(cè)信息,從組件原型上創(chuàng)建出組件節(jié)點(diǎn)的
JS對(duì)象,即組件的this;將組件注冊(cè)信息中的
data復(fù)制一份,作為組件數(shù)據(jù),即this.data;將這份數(shù)據(jù)結(jié)合組件
WXML,據(jù)此創(chuàng)建出Shadow Tree(組件的節(jié)點(diǎn)樹),由于Shadow Tree中可能引用有其他組件,因而這會(huì)遞歸觸發(fā)其他組件創(chuàng)建過程;將
ShadowTree拼接到Composed Tree(最終拼接成的頁面節(jié)點(diǎn)樹)上,并生成一些緩存數(shù)據(jù)用于優(yōu)化組件更新性能;觸發(fā)組件的
created生命周期函數(shù);如果不是頁面根組件,需要根據(jù)組件節(jié)點(diǎn)上的屬性定義,來設(shè)置組件的屬性值;
當(dāng)組件實(shí)例被展示在頁面上時(shí),觸發(fā)組件的
attached生命周期函數(shù),如果Shadow Tree中有其他組件,也逐個(gè)觸發(fā)它們的生命周期函數(shù)。
組件通信
由于業(yè)務(wù)的負(fù)責(zé)度,我們常常需要把一個(gè)大型頁面拆分為多個(gè)組件,多個(gè)組件之間需要進(jìn)行數(shù)據(jù)通信。
對(duì)于跨代組件通信可以考慮全局狀態(tài)管理,這里只討論常見的父子組件通信:
方法一 WXML 數(shù)據(jù)綁定
用于父組件向子組件的指定屬性設(shè)置數(shù)據(jù)。
子聲明 properties 屬性
Component({
properties: {
vtabs: {type: Array, value: []}, // 數(shù)據(jù)項(xiàng)格式為 `{title}`
}
})父組件調(diào)用:
<vtabs vtabs="{{ vtabs }}"</vtabs>方法二 事件
用于子組件向父組件傳遞數(shù)據(jù),可以傳遞任意數(shù)據(jù)。
子組件派發(fā)事件,先在 wxml 結(jié)構(gòu)綁定子組件的點(diǎn)擊事件:
<view bindtap="handleTabClick">
再在 js 文件中進(jìn)行派發(fā)事件,事件名可以自定義填寫, 第二個(gè)參數(shù)可以傳遞數(shù)據(jù)對(duì)象,第三個(gè)參數(shù)為事件選項(xiàng)。
handleClick(e) {
this.triggerEvent(
'tabclick',
{ index },
{
bubbles: false, // 事件是否冒泡
// 事件是否可以穿越組件邊界,為 false 時(shí),事件只在引用組件的節(jié)點(diǎn)樹上觸發(fā),
// 不進(jìn)入其他任何組件的內(nèi)部
composed: false,
capturePhase: false // 事件是否擁有捕獲階段
}
);
},
handleChange(e) {
this.triggerEvent('tabchange', { index });
},最后,在父組件中監(jiān)聽使用:
<vtabs
vtabs="{{ vtabs }}"
bindtabclick="handleTabClick"
bindtabchange="handleTabChange"
>方法三 selectComponent 獲取組件實(shí)例對(duì)象
通過 selectComponent 方法可以獲取子組件的實(shí)例,從而調(diào)用子組件的方法。
父組件的 wxml
<view>
<vtabs-content="goods-content{{ index }}"></vtabs-content>
</view>父組件的 js
Page({
reCalcContentHeight(index) {
const goodsContent = this.selectComponent(`#goods-content${index}`);
},
})selector類似于 CSS 的選擇器,但僅支持下列語法。
ID選擇器:
#the-id(筆者只測(cè)試了這個(gè),其他讀者可自行測(cè)試)class選擇器(可以連續(xù)指定多個(gè)):
.a-class.another-class子元素選擇器:
.the-parent > .the-child后代選擇器:
.the-ancestor .the-descendant跨自定義組件的后代選擇器:
.the-ancestor >>> .the-descendant多選擇器的并集:
#a-node,.some-other-nodes
方法四 url 參數(shù)通信

在電商/物流等微信小程序中,會(huì)存在這樣的用戶故事,有一個(gè)「下單頁面A」和「貨物信息頁面B」
在「下單頁面 A」填寫基本信息,需要下鉆到「詳細(xì)頁面B」填寫詳細(xì)信息的情況。比如一個(gè)寄快遞下單頁面,需要下鉆到貨物信息頁面填寫更詳細(xì)的信息,然后返回上一個(gè)頁面。
在「下單頁面 A」下鉆到「貨物頁面B」,需要回顯「貨物頁面B」的數(shù)據(jù)。
微信小程序由一個(gè) App() 實(shí)例和多個(gè) Page() 組成。小程序框架以棧的方式維護(hù)頁面(最多10個(gè)) 提供了以下 API 進(jìn)行頁面跳轉(zhuǎn),頁面路由如下
wx.navigateTo(只能跳轉(zhuǎn)位于棧內(nèi)的頁面)
wx.redirectTo(可跳轉(zhuǎn)位于棧外的新頁面,并替代當(dāng)前頁面)
wx.navigateBack(返回上一層頁面,不能攜帶參數(shù))
wx.switchTab(切換 Tab 頁面,不支持 url 參數(shù))
wx.reLaunch(小程序重啟)
可以簡(jiǎn)單封裝一個(gè) jumpTo 跳轉(zhuǎn)函數(shù),并傳遞參數(shù):
export function jumpTo(url, options) {
const baseUrl = url.split('?')[0];
// 如果 url 帶了參數(shù),需要把參數(shù)也掛載到 options 上
if (url.indexof('?') !== -1) {
const { queries } = resolveUrl(url);
Object.assign(options, queries, options); // options 的優(yōu)先級(jí)最高
}
cosnt queryString = objectEntries(options)
.filter(item => item[1] || item[0] === 0) // 除了數(shù)字 0 外,其他非值都過濾
.map(
([key, value]) => {
if (typeof value === 'object') {
// 對(duì)象轉(zhuǎn)字符串
value = JSON.stringify(value);
}
if (typeof value === 'string') {
// 字符串 encode
value = encodeURIComponent(value);
}
return `${key}=${value}`;
}
).join('&');
if (queryString) { // 需要組裝參數(shù)
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 輔助函數(shù):
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) {
// 不帶參數(shù)的 url
return {
queries: {},
page: url
}
}
const [page, search] = url.split('?');
const queries = resolveSearch(search);
return {
page,
queries
};
};在「下單頁面A」傳遞數(shù)據(jù):
jumpTo({
url: 'pages/consignment/index',
{
sender: { name: 'naluduo233' }
}
});在「貨物信息頁面B」獲得 URL 參數(shù):
const sender = JSON.parse(getParam('sender') || '{}');url 參數(shù)獲取輔助函數(shù)
// 返回當(dāng)前頁面
export function getCurrentPage() {
const pageStack = wx.getCurrentPages();
const lastIndex = pageStack.length - 1;
const currentPage = pageStack[lastIndex];
return currentPage;
}
// 獲取頁面 url 參數(shù)
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];
}參數(shù)過長怎么辦?路由 api 不支持?jǐn)y帶參數(shù)呢?
雖然微信小程序官方文檔沒有說明可以頁面攜帶的參數(shù)有多長,但還是可能會(huì)有參數(shù)過長被截?cái)嗟娘L(fēng)險(xiǎn)。
我們可以使用全局?jǐn)?shù)據(jù)記錄參數(shù)值,同時(shí)解決 url 參數(shù)過長和路由 api 不支持?jǐn)y帶參數(shù)的問題。
// global-data.js
// 由于 switchTab 不支持?jǐn)y帶參數(shù),所以需要考慮使用全局?jǐn)?shù)據(jù)存儲(chǔ)
// 這里不管是不是 switchTab,先把數(shù)據(jù)掛載上去
const queryMap = {
page: '',
queries: {}
};更新跳轉(zhuǎn)函數(shù)
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 參數(shù)獲取輔助函數(shù)
// 獲取頁面 url 參數(shù)
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)) {
+ // 是 tab-bar 頁面,使用掛載到全局的參數(shù)
+ const { page, queries } = queryMap;
+ if (page === `${route}`) {
+ Object.assign(allParams, queries);
+ }
+ }
}
return allParams;
}輔助函數(shù)
// 判斷當(dāng)前路徑是否是 tabBar
const { tabBar} = appConfig;
export isTabBar = (route) => tabBar.list.some(({ pagePath })) => pagePath === route);按照這樣的邏輯的話,是不是都不用區(qū)分是否是 isTabBar 頁面了,全部頁面都從 queryMap 中獲取?這個(gè)問題目前后續(xù)探究再下結(jié)論,因?yàn)槲夷壳斑€沒試過從頁面實(shí)例的 options 中拿到的值是缺少的。所以可以先保留讀取 getCurrentPages 的值。
方法五 EventChannel 事件派發(fā)通信
前面我談到從「當(dāng)前頁面A」傳遞數(shù)據(jù)到被打開的「頁面B」可以通過 url 參數(shù)。那么想獲取被打開頁面?zhèn)魉偷疆?dāng)前頁面的數(shù)據(jù)要如何做呢?是否也可以通過 url 參數(shù)呢?
答案是可以的,前提是不需要保存「頁面A」的狀態(tài)。如果要保留「頁面 A」的狀態(tài),就需要使用 navigateBack 返回上一頁,而這個(gè) api 是不支持?jǐn)y帶 url 參數(shù)的。
這樣時(shí)候可以使用 頁面間事件通信通道 EventChannel。
pageA 頁面
//
wx.navigateTo({
url: 'pageB?id=1',
events: {
// 為指定事件添加一個(gè)監(jiān)聽器,獲取被打開頁面?zhèn)魉偷疆?dāng)前頁面的數(shù)據(jù)
acceptDataFromOpenedPage: function(data) {
console.log(data)
},
},
success: function(res) {
// 通過eventChannel向被打開頁面?zhèn)魉蛿?shù)據(jù)
res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' })
}
});pageB 頁面
Page({
onLoad: function(option){
const eventChannel = this.getOpenerEventChannel()
eventChannel.emit('acceptDataFromOpenedPage', {data: 'test'});
// 監(jiān)聽acceptDataFromOpenerPage事件,獲取上一頁面通過eventChannel傳送到當(dāng)前頁面的數(shù)據(jù)
eventChannel.on('acceptDataFromOpenerPage', function(data) {
console.log(data)
})
}
})會(huì)出現(xiàn)數(shù)據(jù)無法監(jiān)聽的情況嗎?
小程序的棧不超過 10 層,如果當(dāng)前「頁面A」不是第 10 層,那么可以使用 navigateTo 跳轉(zhuǎn)保留當(dāng)前頁面,跳轉(zhuǎn)到「頁面B」,這個(gè)時(shí)候「頁面B」填寫完畢后傳遞數(shù)據(jù)給「頁面A」時(shí),「頁面A」是可以監(jiān)聽到數(shù)據(jù)的。
如果當(dāng)前「頁面A」已經(jīng)是第10個(gè)頁面,只能使用 redirectTo 跳轉(zhuǎn)「PageB」頁面。結(jié)果是當(dāng)前「頁面A」出棧,新「頁面B」入棧。這個(gè)時(shí)候?qū)ⅰ疙撁鍮」傳遞數(shù)據(jù)給「頁面A」,調(diào)用 navigateBack 是無法回到目標(biāo)「頁面A」的,因此數(shù)據(jù)是無法正常被監(jiān)聽到。
不過我分析做過的小程序中,棧中很少有10層的情況,5 層的也很少。因?yàn)檎{(diào)用 wx.navigateBack 、wx.redirectTo 會(huì)關(guān)閉當(dāng)前頁面,調(diào)用 wx.switchTab 會(huì)關(guān)閉其他所有非 tabBar 頁面。
所以很少會(huì)出現(xiàn)這樣無法回到上一頁面以監(jiān)聽到數(shù)據(jù)的情況,如果真出現(xiàn)這種情況,首先要考慮的不是數(shù)據(jù)的監(jiān)聽問題了,而是要保證如何能夠返回上一頁面。
比如在「PageA」頁面中先調(diào)用 getCurrentPages 獲取頁面的數(shù)量,再把其他的頁面刪除,之后在跳轉(zhuǎn)「PageB」頁面,這樣就避免「PageA」調(diào)用 wx.redirectTo導(dǎo)致關(guān)閉「PageA」。但是官方是不推薦開發(fā)者手動(dòng)更改頁面棧的,需要慎重。
如果有讀者遇到這種情況,并知道如何解決這種的話,麻煩告知下,感謝。
使用自定義的事件中心 EventBus
除了使用官方提供的 EventChannel 外,我們也可以自定義一個(gè)全局的 EventBus 事件中心。 因?yàn)檫@樣更加靈活,不需要在調(diào)用 wx.navigateTo 等APi里傳入?yún)?shù),多平臺(tái)的遷移性更強(qiáng)。
export default class EventBus {
private defineEvent = {};
// 注冊(cè)事件
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);
}
}}
}
// on 監(jiān)聽
public on(event: string, cb): void {
return this.register(event, cb);
}
// off 方法
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) {
if(cb == this.defineEvent[event][i]) {
this.defineEvent[event][i] = null; // 標(biāo)記為空 - 防止dispath 長度變化
// 延時(shí)刪除對(duì)應(yīng)事件
setTimeout(() => this.defineEvent[event].splice(i, 1), 0);
break;
}
}
}
}
}
// once 方法,監(jiān)聽一次
public once(event: string, cb): void {
let onceCb = arg => {
cb && cb(arg);
this.off(event, onceCb);
}
this.register(event, onceCb);
}
// 清空所有事件
public clean(): void {
this.defineEvent = {};
}
}
export connst eventBus = new EventBus();在 PageA 頁面監(jiān)聽:
eventBus.on('update', (data) => console.log(data));在 PageB 頁面派發(fā)
eventBus.dispatch('someEvent', { name: 'naluduo233'});小結(jié)
本文主要討論了微信小程序如何自定義組件,涉及兩個(gè)方面:
組件的聲明與使用
組件的通信
如果你使用的是 taro 的話,直接按照 react 的語法自定義組件就好。而其中的組件通信的話,因?yàn)?taro 最終也是會(huì)編譯為微信小程序,所以 url 和 eventbus 的頁面組件通信方式是適用的。后續(xù)會(huì)分析 vant-ui weapp 的一些組件源碼,看看有贊是如何實(shí)踐的。






