微信小程序中怎么自定义组件?下面本篇文章给大家介绍一下微信小程序中自定义组件的方法,希望对大家有所帮助!
在微信小程序开发过程中,对于一些可能在多个页面都使用的页面模块,可以把它封装成一个组件,以提高开发效率。虽然说我们可以引入整个组件库比如 weui、vant 等,但有时候考虑微信小程序的包体积限制问题,通常封装为自定义的组件更为可控。
并且对于一些业务模块,我们就可以封装为组件复用。本文主要讲述以下两个方面:
- 组件的声明与使用
- 组件通信
组件的声明与使用
微信小程序的组件系统底层是通过 exparser 组件框架实现,它内置在小程序的基础库中,小程序内的所有组件,包括内置组件和自定义组件都由 exparser 组织管理。
自定义组件和写页面一样包含以下几种文件:
- index.json
- index.wxml
- index.wxss
- index.js
- index.wxs
以编写一个 tab 组件为例: 编写自定义组件时需要在 json 文件中讲 component 字段设为 true:
{ "component": true }
在 js 文件中,基础库提供有 page 和 component 两个构造器,page 对应的页面为页面根组件,component 则对应:
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 的小伙伴,会发现这个声明很熟悉。
在小程序启动时,构造器会将开发者设置的properties、data、methods等定义段,
写入exparser的组件注册表中。这个组件在被其它组件引用时,就可以根据这些注册信息来创建自定义组件的实例。
模版文件 wxml:
<view> <slot></slot></view>
样式文件:
.vtabs {}
外部页面组件使用,只需要在页面的 json 文件中引入
{ "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 属性
component({ properties: { vtabs: {type: array, value: []}, // 数据项格式为 `{title}` } })
父组件调用:
<vtabs></vtabs>
方法二 事件
用于子组件向父组件传递数据,可以传递任意数据。
子组件派发事件,先在 wxml 结构绑定子组件的点击事件:
<view></view>
再在 js 文件中进行派发事件,事件名可以自定义填写, 第二个参数可以传递数据对象,第三个参数为事件选项。
handleclick(e) { this.triggerevent( 'tabclick', { index }, { bubbles: false, // 事件是否冒泡 // 事件是否可以穿越组件边界,为 false 时,事件只在引用组件的节点树上触发, // 不进入其他任何组件的内部 composed: false, capturephase: false // 事件是否拥有捕获阶段 } ); }, handlechange(e) { this.triggerevent('tabchange', { index }); },
最后,在父组件中监听使用:
<vtabs></vtabs>
方法三 selectcomponent 获取组件实例对象
通过 selectcomponent 方法可以获取子组件的实例,从而调用子组件的方法。
父组件的 wxml
<view> <vtabs-content></vtabs-content></view>
父组件的 js
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 进行页面跳转,exparser如下
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 { 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 { 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 参数的。
这样时候可以使用 exparser 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> 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();</len>
在 pagea 页面监听:
eventbus.on('update', (data) => console.log(data));
在 pageb 页面派发
eventbus.dispatch('someevent', { name: 'naluduo233'});
小结
本文主要讨论了微信小程序如何自定义组件,涉及两个方面:
- 组件的声明与使用
- 组件的通信
如果你使用的是 taro 的话,直接按照 react 的语法自定义组件就好。而其中的组件通信的话,因为 taro 最终也是会编译为微信小程序,所以 url 和 eventbus 的页面组件通信方式是适用的。后续会分析 vant-ui weapp 的一些组件源码,看看有赞是如何实践的。
感谢阅读,如有错误的地方请指出
【相关学习推荐:exparser】
以上就是浅析微信小程序中自定义组件的方法的详细内容,更多请关注代码网其它相关文章!
发表评论