WeChat ミニ プログラムのコンポーネントをカスタマイズするにはどうすればよいですか?次の記事では、WeChat ミニ プログラムのコンポーネントをカスタマイズする方法を紹介します。
WeChat アプレットの開発プロセス中、開発効率を向上させるために、複数のページで使用される可能性のある一部のページ モジュールをコンポーネントにカプセル化できます。 weui、vant などのコンポーネント ライブラリ全体を導入することもできますが、WeChat アプレットのパッケージ サイズ制限を考慮する場合もありますが、通常はカスタム コンポーネントとしてカプセル化する方が制御しやすいです。
また、一部のビジネス モジュールについては、コンポーネントとしてカプセル化して再利用できます。この記事では主に次の 2 つの側面について説明します。
WeChat ミニ プログラムのコンポーネント システムの最下層は、ミニ プログラムの基本ライブラリに組み込まれている Exparser コンポーネント フレームワークを通じて実装されます。ミニ プログラムには組み込みコンポーネントとカスタム コンポーネントが含まれており、すべて Exparser 組織によって管理されます。
カスタム コンポーネントには、ページの作成と同様に次のファイルが含まれます:
tab
コンポーネントの作成を例として挙げます。
カスタム コンポーネントを作成するときは、json
ファイルの component
フィールドを true
:
{ "component": true }
に設定する必要があります。
このファイルでは、基本ライブラリには Page と Component という 2 つのコンストラクターが用意されています。Page に対応するページはページ ルート コンポーネントで、コンポーネントは次のものに対応します。 Component({ options: { // 组件配置 addGlobalClass: true, // 指定所有 _ 开头的数据字段为纯数据字段 // 纯数据字段是一些不用于界面渲染的 data 字段,可以用于提升页面更新性能 pureDataPattern: /^_/, multipleSlots: true // 在组件定义时的选项中启用多slot支持 }, 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 を知っている友人がいる場合は、このステートメントは非常によく知られていることがわかります。
ミニ プログラムが開始されると、コンストラクターは
開発者によって設定されたプロパティ、データ、メソッド、およびその他の定義セクションを Exparser のコンポーネント レジストリに書き込みます。このコンポーネントが他のコンポーネントによって参照される場合、これらの登録情報に基づいてカスタム コンポーネントのインスタンスを作成できます。
テンプレート ファイル wxml:
<view class='vtabs'> <slot /> </view>
スタイル ファイル:
.vtabs {}
外部ページ コンポーネントを使用するには、json
に
{ "navigationBarTitleText": "商品分类", "usingComponents": { "vtabs": "../../../components/vtabs", } }
ページを初期化すると、Exparser はページ ルート コンポーネントのインスタンスを作成し、使用される他のコンポーネントもコンポーネント インスタンスを作成することで応答します (これは再帰的なプロセスです):
コンポーネント作成の流れ 大きく分けて以下のような流れになります。
コンポーネントの登録情報に従い、コンポーネントのプロトタイプからコンポーネントノードの JS
オブジェクトを作成します。コンポーネントの this
;
コンポーネント登録情報の data
をコンポーネントデータ、つまり this としてコピーします。 .data
;
このデータをコンポーネント WXML
と結合し、Shadow Tree
( Shadow Tree
他のコンポーネントを参照する可能性があるため、これにより他のコンポーネントの作成プロセスが再帰的にトリガーされます;
ShadowTree
を に接続します合成ツリー
(最終的に結合されたページ ノード ツリー)、コンポーネントの更新パフォーマンスを最適化するためにいくつかのキャッシュ データを生成します。
作成された をトリガーします。コンポーネントのライフサイクル関数;
attached ライフ サイクル関数がトリガーされます。
Shadow Tree に他のコンポーネントがある場合は、 、それらのライフサイクル関数も 1 つずつトリガーされます。
方法 1 WXML データ バインディング
データを子コンポーネントの指定されたプロパティに設定するために親コンポーネントによって使用されます。 子宣言プロパティComponent({ properties: { vtabs: {type: Array, value: []}, // 数据项格式为 `{title}` } })
<vtabs vtabs="{{ vtabs }}"</vtabs>
メソッド 2 イベント
用途子コンポーネントが親コンポーネントにデータを渡すには、任意のデータを渡すことができます。 サブコンポーネントからイベントをディスパッチするには、まずサブコンポーネントのクリック イベントを wxml 構造にバインドします:<view bindtap="handleTabClick">
handleClick(e) { this.triggerEvent( 'tabclick', { index }, { bubbles: false, // 事件是否冒泡 // 事件是否可以穿越组件边界,为 false 时,事件只在引用组件的节点树上触发, // 不进入其他任何组件的内部 composed: false, capturePhase: false // 事件是否拥有捕获阶段 } ); }, handleChange(e) { this.triggerEvent('tabchange', { index }); },
<vtabs vtabs="{{ vtabs }}" bindtabclick="handleTabClick" bindtabchange="handleTabChange" >
#メソッド 3 selectComponent を使用して、コンポーネント インスタンス オブジェクトを取得します# #PassselectComponent
メソッドはサブコンポーネントのインスタンスを取得できるため、サブコンポーネントのメソッドを呼び出すことができます。親コンポーネントの wxml
<view> <vtabs-content="goods-content{{ index }}"></vtabs-content> </view>
Page({ reCalcContentHeight(index) { const goodsContent = this.selectComponent(`#goods-content${index}`); }, })
#the-id
(笔者只测试了这个,其他读者可自行测试).a-class.another-class
.the-parent > .the-child
.the-ancestor .the-descendant
.the-ancestor >>> .the-descendant
#a-node
, .some-other-nodes
方法四 url 参数通信
在电商/物流等微信小程序中,会存在这样的用户故事,有一个「下单页面A」和「货物信息页面B」
微信小程序由一个 App()
实例和多个 Page()
组成。小程序框架以栈的方式维护页面(最多10个) 提供了以下 API 进行页面跳转,页面路由如下
wx.navigateTo(只能跳转位于栈内的页面)
wx.redirectTo(可跳转位于栈外的新页面,并替代当前页面)
wx.navigateBack(返回上一层页面,不能携带参数)
wx.switchTab(切换 Tab 页面,不支持 url 参数)
wx.reLaunch(小程序重启)
可以简单封装一个 jumpTo 跳转函数,并传递参数:
export function jumpTo(url, options) { const baseUrl = url.split('?')[0]; // 如果 url 带了参数,需要把参数也挂载到 options 上 if (url.indexof('?') !== -1) { const { queries } = resolveUrl(url); Object.assign(options, queries, options); // options 的优先级最高 } cosnt queryString = objectEntries(options) .filter(item => item[1] || item[0] === 0) // 除了数字 0 外,其他非值都过滤 .map( ([key, value]) => { if (typeof value === 'object') { // 对象转字符串 value = JSON.stringify(value); } if (typeof value === 'string') { // 字符串 encode 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 辅助函数:
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) { // 不带参数的 url return { queries: {}, page: url } } const [page, search] = url.split('?'); const queries = resolveSearch(search); return { page, queries }; };
在「下单页面A」传递数据:
jumpTo({ url: 'pages/consignment/index', { sender: { name: 'naluduo233' } } });
在「货物信息页面B」获得 URL 参数:
const sender = JSON.parse(getParam('sender') || '{}');
url 参数获取辅助函数
// 返回当前页面 export function getCurrentPage() { const pageStack = wx.getCurrentPages(); const lastIndex = pageStack.length - 1; const currentPage = pageStack[lastIndex]; return currentPage; } // 获取页面 url 参数 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 不支持携带参数的问题。
// global-data.js // 由于 switchTab 不支持携带参数,所以需要考虑使用全局数据存储 // 这里不管是不是 switchTab,先把数据挂载上去 const queryMap = { page: '', queries: {} };
更新跳转函数
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 参数获取辅助函数
// 获取页面 url 参数 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 页面,使用挂载到全局的参数 + const { page, queries } = queryMap; + if (page === `${route}`) { + Object.assign(allParams, queries); + } + } } return allParams; }
辅助函数
// 判断当前路径是否是 tabBar 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 页面
// wx.navigateTo({ url: 'pageB?id=1', events: { // 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据 acceptDataFromOpenedPage: function(data) { console.log(data) }, }, success: function(res) { // 通过eventChannel向被打开页面传送数据 res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' }) } });
pageB 页面
Page({ onLoad: function(option){ const eventChannel = this.getOpenerEventChannel() eventChannel.emit('acceptDataFromOpenedPage', {data: 'test'}); // 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据 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里传入参数,多平台的迁移性更强。
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 监听 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; // 标记为空 - 防止dispath 长度变化 // 延时删除对应事件 setTimeout(() => this.defineEvent[event].splice(i, 1), 0); break; } } } } } // once 方法,监听一次 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 页面监听:
eventBus.on('update', (data) => console.log(data));
在 PageB 页面派发
eventBus.dispatch('someEvent', { name: 'naluduo233'});
本文主要讨论了微信小程序如何自定义组件,涉及两个方面:
如果你使用的是 taro 的话,直接按照 react 的语法自定义组件就好。而其中的组件通信的话,因为 taro 最终也是会编译为微信小程序,所以 url 和 eventbus 的页面组件通信方式是适用的。后续会分析 vant-ui weapp 的一些组件源码,看看有赞是如何实践的。
感谢阅读,如有错误的地方请指出
【相关学习推荐:小程序开发教程】
以上がWeChat ミニ プログラムのコンポーネントをカスタマイズする方法の簡単な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。