一步步教你實現(xiàn)微信小程序自定義組件
前言
在微信小程序開發(fā)過程中,對于一些可能在多個頁面都使用的頁面模塊,可以把它封裝成一個組件,以提高開發(fā)效率。雖然說我們可以引入整個組件庫比如 weui、vant 等,但有時候考慮微信小程序的包體積限制問題,通常封裝為自定義的組件更為可控。
并且對于一些業(yè)務(wù)模塊,我們就可以封裝為組件復(fù)用。本文主要講述以下兩個方面:
- 組件的聲明與使用
- 組件通信
組件的聲明與使用
微信小程序的組件系統(tǒng)底層是通過 Exparser 組件框架實現(xiàn),它內(nèi)置在小程序的基礎(chǔ)庫中,小程序內(nèi)的所有組件,包括內(nèi)置組件和自定義組件都由 Exparser 組織管理。
自定義組件和寫頁面一樣包含以下幾種文件:
- index.json
- index.wxml
- index.wxss
- index.js
- index.wxs
以編寫一個 tab 組件為例: 編寫自定義組件時需要在 json 文件中講 component 字段設(shè)為 true:
{ "component": true }
在 js 文件中,基礎(chǔ)庫提供有 Page 和 Component 兩個構(gòu)造器,Page 對應(yīng)的頁面為頁面根組件,Component 則對應(yīng):
Component({ options: { // 組件配置 addGlobalClass: true, // 指定所有 _ 開頭的數(shù)據(jù)字段為純數(shù)據(jù)字段 // 純數(shù)據(jù)字段是一些不用于界面渲染的 data 字段,可以用于提升頁面更新性能 pureDataPattern: /^_/, multipleSlots: true // 在組件定義時的選項中啟用多slot支持 }, properties: { vtabs: {type: Array, value: []}, }, data: { currentView: 0, }, observers: { // 監(jiān)測 activeTab: function(activeTab) { this.scrollTabBar(activeTab); } }, relations: { // 關(guān)聯(lián)的子/父組件 '../vtabs-content/index': { type: 'child', // 關(guān)聯(lián)的目標(biāo)節(jié)點應(yīng)為子節(jié)點 linked: function(target) { this.calcVtabsCotentHeight(target); }, unlinked: function(target) { delete this.data._contentHeight[target.data.tabIndex]; } } }, lifetimes: { // 組件聲明周期 created: function() { // 組件實例剛剛被創(chuàng)建好時 }, attached: function() { // 在組件實例進入頁面節(jié)點樹時執(zhí)行 }, detached: function() { // 在組件實例被從頁面節(jié)點樹移除時執(zhí)行 }, }, methods: { // 組件方法 calcVtabsCotentHeight(target) {} } });
如果有了解過 Vue2 的小伙伴,會發(fā)現(xiàn)這個聲明很熟悉。
在小程序啟動時,構(gòu)造器會將開發(fā)者設(shè)置的properties、data、methods等定義段,
寫入Exparser的組件注冊表中。這個組件在被其它組件引用時,就可以根據(jù)這些注冊信息來創(chuàng)建自定義組件的實例。
模版文件 wxml:
<view class='vtabs'> <slot /> </view>
樣式文件:
.vtabs {}
外部頁面組件使用,只需要在頁面的 json 文件中引入
{ "navigationBarTitleText": "商品分類", "usingComponents": { "vtabs": "../../../components/vtabs", } }
在初始化頁面時,Exparser 會創(chuàng)建出頁面根組件的一個實例,用到的其他組件也會響應(yīng)創(chuàng)建組件實例(這是一個遞歸的過程):
組件創(chuàng)建的過程大致有以下幾個要點:
- 根據(jù)組件注冊信息,從組件原型上創(chuàng)建出組件節(jié)點的 JS 對象,即組件的 this;
- 將組件注冊信息中的 data 復(fù)制一份,作為組件數(shù)據(jù),即 this.data;
- 將這份數(shù)據(jù)結(jié)合組件 WXML,據(jù)此創(chuàng)建出 Shadow Tree(組件的節(jié)點樹),由于 Shadow Tree 中可能引用有其他組件,因而這會遞歸觸發(fā)其他組件創(chuàng)建過程;
- 將 ShadowTree 拼接到 Composed Tree(最終拼接成的頁面節(jié)點樹)上,并生成一些緩存數(shù)據(jù)用于優(yōu)化組件更新性能;
- 觸發(fā)組件的 created 生命周期函數(shù);
- 如果不是頁面根組件,需要根據(jù)組件節(jié)點上的屬性定義,來設(shè)置組件的屬性值;
- 當(dāng)組件實例被展示在頁面上時,觸發(fā)組件的 attached 生命周期函數(shù),如果 Shadow Tree 中有其他組件,也逐個觸發(fā)它們的生命周期函數(shù)。
組件通信
由于業(yè)務(wù)的負責(zé)度,我們常常需要把一個大型頁面拆分為多個組件,多個組件之間需要進行數(shù)據(jù)通信。
對于跨代組件通信可以考慮全局狀態(tài)管理,這里只討論常見的父子組件通信:
方法一 WXML 數(shù)據(jù)綁定
用于父組件向子組件的指定屬性設(shè)置數(shù)據(jù)。
子聲明 properties 屬性
Component({ properties: { vtabs: {type: Array, value: []}, // 數(shù)據(jù)項格式為 `{title}` } })
父組件調(diào)用:
<vtabs vtabs="{{ vtabs }}"</vtabs>
方法二 事件
用于子組件向父組件傳遞數(shù)據(jù),可以傳遞任意數(shù)據(jù)。
子組件派發(fā)事件,先在 wxml 結(jié)構(gòu)綁定子組件的點擊事件:
<view bindtap="handleTabClick">
再在 js 文件中進行派發(fā)事件,事件名可以自定義填寫, 第二個參數(shù)可以傳遞數(shù)據(jù)對象,第三個參數(shù)為事件選項。
handleClick(e) { this.triggerEvent( 'tabclick', { index }, { bubbles: false, // 事件是否冒泡 // 事件是否可以穿越組件邊界,為 false 時,事件只在引用組件的節(jié)點樹上觸發(fā), // 不進入其他任何組件的內(nèi)部 composed: false, capturePhase: false // 事件是否擁有捕獲階段 } ); }, handleChange(e) { this.triggerEvent('tabchange', { index }); },
最后,在父組件中監(jiān)聽使用:
<vtabs vtabs="{{ vtabs }}" bindtabclick="handleTabClick" bindtabchange="handleTabChange" >
方法三 selectComponent 獲取組件實例對象
通過 selectComponent 方法可以獲取子組件的實例,從而調(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(筆者只測試了這個,其他讀者可自行測試)
- class選擇器(可以連續(xù)指定多個):.a-class.another-class
- 子元素選擇器:.the-parent > .the-child
- 后代選擇器:.the-ancestor .the-descendant
- 跨自定義組件的后代選擇器:.the-ancestor >>> .the-descendant
- 多選擇器的并集:#a-node, .some-other-nodes
方法四 url 參數(shù)通信
在電商/物流等微信小程序中,會存在這樣的用戶故事,有一個「下單頁面A」和「貨物信息頁面B」
- 在「下單頁面 A」填寫基本信息,需要下鉆到「詳細頁面B」填寫詳細信息的情況。比如一個寄快遞下單頁面,需要下鉆到貨物信息頁面填寫更詳細的信息,然后返回上一個頁面。
- 在「下單頁面 A」下鉆到「貨物頁面B」,需要回顯「貨物頁面B」的數(shù)據(jù)。
微信小程序由一個 App()
實例和多個 Page()
組成。小程序框架以棧的方式維護頁面(最多10個) 提供了以下 API 進行頁面跳轉(zhuǎn),頁面路由如下
- wx.navigateTo(只能跳轉(zhuǎn)位于棧內(nèi)的頁面)
- wx.redirectTo(可跳轉(zhuǎn)位于棧外的新頁面,并替代當(dāng)前頁面)
- wx.navigateBack(返回上一層頁面,不能攜帶參數(shù))
- wx.switchTab(切換 Tab 頁面,不支持 url 參數(shù))
- wx.reLaunch(小程序重啟)
可以簡單封裝一個 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)先級最高 } cosnt queryString = objectEntries(options) .filter(item => item[1] || item[0] === 0) // 除了數(shù)字 0 外,其他非值都過濾 .map( ([key, value]) => { if (typeof value === 'object') { // 對象轉(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 不支持攜帶參數(shù)呢?
雖然微信小程序官方文檔沒有說明可以頁面攜帶的參數(shù)有多長,但還是可能會有參數(shù)過長被截斷的風(fēng)險。
我們可以使用全局數(shù)據(jù)記錄參數(shù)值,同時解決 url 參數(shù)過長和路由 api 不支持攜帶參數(shù)的問題。
// global-data.js // 由于 switchTab 不支持攜帶參數(shù),所以需要考慮使用全局數(shù)據(jù)存儲 // 這里不管是不是 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 中獲取?這個問題目前后續(xù)探究再下結(jié)論,因為我目前還沒試過從 頁面示例的 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 返回上一頁,而這個 api 是不支持攜帶 url 參數(shù)的。
這樣時候可以使用 頁面間事件通信通道 EventChannel。
pageA 頁面
// wx.navigateTo({ url: 'pageB?id=1', events: { // 為指定事件添加一個監(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) }) } })
會出現(xiàn)數(shù)據(jù)無法監(jiān)聽的情況嗎?
小程序的棧不超過 10 層,如果當(dāng)前「頁面A」不是第 10 層,那么可以使用 navigateTo 跳轉(zhuǎn)保留當(dāng)前頁面,跳轉(zhuǎn)到「頁面B」,這個時候「頁面B」填寫完畢后傳遞數(shù)據(jù)給「頁面A」時,「頁面A」是可以監(jiān)聽到數(shù)據(jù)的。
如果當(dāng)前「頁面A」已經(jīng)是第10個頁面,只能使用 redirectTo 跳轉(zhuǎn)「PageB」頁面。結(jié)果是當(dāng)前「頁面A」出棧,新「頁面B」入棧。這個時候?qū)ⅰ疙撁鍮」傳遞數(shù)據(jù)給「頁面A」,調(diào)用 navigateBack 是無法回到目標(biāo)「頁面A」的,因此數(shù)據(jù)是無法正常被監(jiān)聽到。
不過我分析做過的小程序中,棧中很少有10層的情況,5 層的也很少。因為調(diào)用 wx.navigateBack 、wx.redirectTo 會關(guān)閉當(dāng)前頁面,調(diào)用 wx.switchTab 會關(guān)閉其他所有非 tabBar 頁面。
所以很少會出現(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ā)者手動更改頁面棧的,需要慎重。
如果有讀者遇到這種情況,并知道如何解決這種的話,麻煩告知下,感謝。
使用自定義的事件中心 EventBus
除了使用官方提供的 EventChannel 外,我們也可以自定義一個全局的 EventBus 事件中心。 因為這樣更加靈活,不需要在調(diào)用 wx.navigateTo
等APi里傳入?yún)?shù),多平臺的遷移性更強。
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); } }} } // 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 長度變化 // 延時刪除對應(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é)
本文主要討論了微信小程序如何自定義組件,涉及兩個方面:
- 組件的聲明與使用
- 組件的通信
如果你使用的是 taro 的話,直接按照 react 的語法自定義組件就好。而其中的組件通信的話,因為 taro 最終也是會編譯為微信小程序,所以 url 和 eventbus 的頁面組件通信方式是適用的。后續(xù)會分析 vant-ui weapp 的一些組件源碼,看看有贊是如何實踐的。
附:組件和頁面的區(qū)別
1、點擊一個文件夾右鍵——新建Page 、新建Component
2、組件的js文件
Component({ properties: { //父組件傳過來的data num: Number, flag:Boolean }, /** * 頁面的初始數(shù)據(jù) */ data: { }, methods:{ //組件的方法要寫在methods中 jia(e) { let a if (this.properties.flag) { a = -1; } else { a = 1; } this.setData({ flag: !this.properties.flag, num: this.properties.num + a }) } },
3、頁面的js文件
Page({ data: { flag:false, number:1, motto: 'Hello World', }, //事件處理函數(shù),直接作為參數(shù) bindViewTap: function() { wx.navigateTo({ url: '../logs/logs' }) }, })
總結(jié)
到此這篇關(guān)于微信小程序自定義組件的文章就介紹到這了,更多相關(guān)微信小程序自定義組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 微信小程序自定義組件實現(xiàn)多選功能
- 微信小程序自定義組件與頁面的相互傳參
- 微信小程序?qū)崿F(xiàn)頁面監(jiān)聽自定義組件的觸發(fā)事件
- 微信小程序自定義組件components(代碼詳解)
- 微信小程序頁面調(diào)用自定義組件內(nèi)的事件詳解
- 詳解微信小程序自定義組件的實現(xiàn)及數(shù)據(jù)交互
- 微信小程序自定義組件實現(xiàn)環(huán)形進度條
- 微信小程序自定義組件傳值 頁面和組件相互傳數(shù)據(jù)操作示例
- 微信小程序自定義組件的實現(xiàn)方法及自定義組件與頁面間的數(shù)據(jù)傳遞問題
- 微信小程序自定義組件封裝及父子間組件傳值的方法
- 微信小程序的自定義組件的實現(xiàn)方法
相關(guān)文章
使用Sticky組件實現(xiàn)帶sticky效果的tab導(dǎo)航和滾動導(dǎo)航的方法
sticky組件,通常應(yīng)用于導(dǎo)航條或者工具欄,當(dāng)網(wǎng)頁在某一區(qū)域滾動的時候,將導(dǎo)航條或工具欄這類元素固定在頁面頂部或底部,方便用戶快速進行這類元素提供的操作2016-03-03JavaScript實現(xiàn)類似拉勾網(wǎng)的鼠標(biāo)移入移出效果
其實也是個偶然的機會讓我想去研究一下這個效果。主要是由于有個群里的人發(fā)了個講解這個效果的鏈接,當(dāng)時也沒怎么在意,然后過兩天,突然就想起這件事,便去拉勾網(wǎng)一看,效果不錯啊,所以就自己研究起來,現(xiàn)在將過程分享給大家,有需要的可以參考借鑒。2016-10-10原生js基于canvas實現(xiàn)一個簡單的前端截圖工具代碼實例
這篇文章主要介紹了原生js基于canvas實現(xiàn)一個簡單的前端截圖工具代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-09-09解決包含在label標(biāo)簽下的checkbox在ie8及以下版本點擊事件無效果兼容的問題
這篇文章主要介紹了解決包含在label標(biāo)簽下的checkbox在ie8及以下版本點擊事件無效果兼容的問題,本文給大家總結(jié)的非常詳細,需要的朋友可以參考下2019-10-10JavaScript中保留小數(shù)點后N位方法總結(jié)
這篇文章主要為大家詳細介紹了JavaScript中保留小數(shù)點后N位的幾個常用方法,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起了解一下2023-06-06