微信小程序中怎么自定义组件?下面本篇文章给大家介绍一下微信小程序中自定义组件的方法,希望对大家有所帮助!
在微信小程序开发过程中,对于一些可能在多个页面都使用的页面模块,可以把它封装成一个组件,以提高开发效率。虽然说我们可以引入整个组件库比如 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: {
& #39;../vtabs-content/index': {
type: & #39;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(
& #39;tabclick',
{ index },
{
bubbles: false ,
composed: false ,
capturePhase: false
}
);
},
handleChange(e) {
this .triggerEvent(& #39;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」
微信小程序由一个 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(& #39;?')[0];
if (url.indexof(& #39;?') !== -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 === & #39;object') {
value = JSON.stringify(value);
}
if ( typeof value === & #39;string') {
value = encodeURIComponent(value);
}
return `${key}=${value}`;
}
).join(& #39;&');
if (queryString) {
url = `${baseUrl}?${queryString}`;
}
const pageCount = wx.getCurrentPages().length;
if (jumpType === & #39;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(& #39;&');
paramList.forEach(param => {
const [key, value = & #39;'] = param.split('=');
queries[key] = value;
});
return queries;
};
export const resolveUrl = (url) => {
if (url.indexOf(& #39;?') === -1) {
return {
queries: {},
page: url
}
}
const [page, search] = url.split(& #39;?');
const queries = resolveSearch(search);
return {
page,
queries
};
};
|
在「下单页面A」传递数据:
1 2 3 4 5 6 | jumpTo({
url: & #39;pages/consignment/index',
{
sender: { name: & #39;naluduo233' }
}
});
|
在「货物信息页面B」获得 URL 参数:
1 | const sender = JSON.parse(getParam(& #39;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: & #39;',
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 === & #39;switchTab') {
wx.switchTab({ url: baseUrl });
} else if (jumpType === & #39;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: & #39;pageB?id=1',
events: {
acceptDataFromOpenedPage: function (data) {
console.log(data)
},
},
success: function (res) {
res.eventChannel.emit(& #39;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(& #39;acceptDataFromOpenedPage', {data: 'test'});
eventChannel.on(& #39;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(& #39;update', (data) => console.log(data));
|
在 PageB 页面派发
1 | eventBus.dispatch(& #39;someEvent', { name: 'naluduo233'});
|
小结
本文主要讨论了微信小程序如何自定义组件,涉及两个方面:
如果你使用的是 taro 的话,直接按照 react 的语法自定义组件就好。而其中的组件通信的话,因为 taro 最终也是会编译为微信小程序,所以 url 和 eventbus 的页面组件通信方式是适用的。后续会分析 vant-ui weapp 的一些组件源码,看看有赞是如何实践的。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
暂时没有评论,来抢沙发吧~