• 技术文章 >微信小程序 >小程序开发

    浅析微信小程序中自定义组件的方法

    青灯夜游青灯夜游2022-03-30 11:13:27转载1574
    微信小程序中怎么自定义组件?下面本篇文章给大家介绍一下微信小程序中自定义组件的方法,希望对大家有所帮助!

    在微信小程序开发过程中,对于一些可能在多个页面都使用的页面模块,可以把它封装成一个组件,以提高开发效率。虽然说我们可以引入整个组件库比如 weui、vant 等,但有时候考虑微信小程序的包体积限制问题,通常封装为自定义的组件更为可控。

    并且对于一些业务模块,我们就可以封装为组件复用。本文主要讲述以下两个方面:

    组件的声明与使用

    微信小程序的组件系统底层是通过 Exparser 组件框架实现,它内置在小程序的基础库中,小程序内的所有组件,包括内置组件和自定义组件都由 Exparser 组织管理。

    自定义组件和写页面一样包含以下几种文件:

    以编写一个 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 class='vtabs'>
        <slot />
    </view>

    样式文件:

    .vtabs {}

    外部页面组件使用,只需要在页面的 json 文件中引入

    {
      "navigationBarTitleText": "商品分类",
      "usingComponents": {
        "vtabs": "../../../components/vtabs",
      }
    }

    在初始化页面时,Exparser 会创建出页面根组件的一个实例,用到的其他组件也会响应创建组件实例(这是一个递归的过程):

    组件创建的过程大致有以下几个要点:

    组件通信

    由于业务的负责度,我们常常需要把一个大型页面拆分为多个组件,多个组件之间需要进行数据通信。

    对于跨代组件通信可以考虑全局状态管理,这里只讨论常见的父子组件通信:

    方法一 WXML 数据绑定

    用于父组件向子组件的指定属性设置数据。

    子声明 properties 属性

    Component({
        properties: {
            vtabs: {type: Array, value: []}, // 数据项格式为 `{title}`
        }
    })

    父组件调用:

        <vtabs vtabs="{{ vtabs }}"</vtabs>

    方法二 事件

    用于子组件向父组件传递数据,可以传递任意数据。

    子组件派发事件,先在 wxml 结构绑定子组件的点击事件:

       <view bindtap="handleTabClick">

    再在 js 文件中进行派发事件,事件名可以自定义填写, 第二个参数可以传递数据对象,第三个参数为事件选项。

     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" 
    >

    方法三 selectComponent 获取组件实例对象

    通过 selectComponent 方法可以获取子组件的实例,从而调用子组件的方法。

    父组件的 wxml

    <view>
        <vtabs-content="goods-content{{ index }}"></vtabs-content>
    </view>

    父组件的 js

    Page({
        reCalcContentHeight(index) {
            const goodsContent = this.selectComponent(`#goods-content${index}`);
        },
    })

    selector类似于 CSS 的选择器,但仅支持下列语法。

    方法四 url 参数通信

    1.png

    在电商/物流等微信小程序中,会存在这样的用户故事,有一个「下单页面A」和「货物信息页面B」

    微信小程序由一个 App() 实例和多个 Page() 组成。小程序框架以栈的方式维护页面(最多10个) 提供了以下 API 进行页面跳转,页面路由如下

    可以简单封装一个 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.navigateBackwx.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 的一些组件源码,看看有赞是如何实践的。

    感谢阅读,如有错误的地方请指出

    【相关学习推荐:小程序开发教程

    以上就是浅析微信小程序中自定义组件的方法的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    上一篇:微信小程序开发底部导航 下一篇:归纳整理微信小程序常用表单组件
    Web大前端开发直播班

    相关文章推荐

    • 手把手教你在微信小程序中使用canvas绘制天气折线图(附代码)• 浅析小程序中什么是behaviors?怎么创建和使用?• 总结分享微信小程序的开发步骤• 零基础微信小程序开发及实例详解• 聊聊小程序怎么实现“全文收起”功能• 微信小程序开发底部导航
    1/1

    PHP中文网