微信小程序中怎么自定义组件

在微信小程序开发过程中,对于一些可能在多个页面都使用的页面模块,可以把它封装成一个组件,以提高开发效率。虽然说我们可以引入整个组件库比如 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 class="vtabs">
 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="ii vtabs }"</vtabs>


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


handleClick(e) {

    this.triggerEvent(

        'tabclick', 

        { index }, 

        { 

            bubbles: false,  // 事件是否冒泡

            // 事件是否可以穿越组件边界,为 false 时,事件只在引用组件的节点树上触发,

            // 不进入其他任何组件的内部

            composed: false,  

            capturePhase: false // 事件是否拥有捕获阶段 

        }

    );

},

handleChange(e) {

    this.triggerEvent('tabchange', { index });

},


最后,在父组件中监听使用:



   vtabs="{{ vtabs }}"

   bindtabclick="handleTabClick"

   bindtabchange="handleTabChange"

>


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


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


父组件的 wxml


  <view>
<vtabs-content="goods-contentfi index }}"></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 进行页面跳转,页面路由如下


  • 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];

}


辅助函数


// 判断当前路径是否是 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)

        })

      }

})


使用自定义的事件中心 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
                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.de

我来说两句

0 条评论

推荐阅读

  • 响应式布局CSS媒体查询设备像素比介绍

    构建响应式网站布局最常见的是流体网格,灵活调整大小的站点布局技术,确保用户在使用的幕上获得完整的体验。响应式设计如何展示富媒体图像,可以通过以下几种方法。

    admin
  • 提升网站的性能快速加载的实用技巧

    网站速度很重要,快速加载的网站会带来更好的用户体验、更高的转化率、更多的参与度,而且在搜索引擎排名中也扮演重要角色,做SEO,网站硬件是起跑线,如果输在了起跑线,又怎么跟同行竞争。有许多方法可提升网站的性能,有一些技巧可以避免踩坑。

    admin
  • 织梦CMS TAG页找不到标签和实现彩色标签解决方法

    织梦cms是我们常见的网站程序系统的一款,在TAG标签中常常遇到的问题也很多。当我们点击 tags.php 页的某个标签的时候,有时会提示:“系统无此标签,可 能已经移除!” 但是我们检查程序后台,以及前台显示页面。这个标签确实存在,如果解决这个问题那?

    admin
  • HTML关于fieldset标签主要的作用

    在前端开发html页面中常用的标签很多,今天为大家带来的是关于HTML中fieldset标签主要的作用说明,根据技术分析HTML

    admin

精选专题