微信小程式中怎麼自訂元件?以下這篇文章為大家介紹一下微信小程式中自訂元件的方法,希望對大家有幫助!

在微信小程式開發過程中,對於一些可能在多個頁面都使用的頁面模組,可以把它封裝成一個元件,以提高開發效率。雖然說我們可以引入整個元件庫例如 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) {
if (cb == this.defineEvent[event][i]) {
this.defineEvent[event][i] = null;
setTimeout(() => this.defineEvent[event].splice(i, 1), 0);
break ;
}
}
}
}
}
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 页面监听:
1 | eventBus.on('update', (data) => console.log(data));
|
登入後複製
在 PageB 页面派发
1 | eventBus.dispatch('someEvent', { name: 'naluduo233'});
|
登入後複製
小结
本文主要讨论了微信小程序如何自定义组件,涉及两个方面:
如果你使用的是 taro 的话,直接按照 react 的语法自定义组件就好。而其中的组件通信的话,因为 taro 最终也是会编译为微信小程序,所以 url 和 eventbus 的页面组件通信方式是适用的。后续会分析 vant-ui weapp 的一些组件源码,看看有赞是如何实践的。
感谢阅读,如有错误的地方请指出
【相关学习推荐:小程序开发教程】
以上是淺析微信小程式中自訂元件的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!