微信小程序之自定义组件

微信小程序出了挺长一段时间了,但我对这个一直被人吹捧的框架没有半点感冒。出于工作需要写了一个商城demo,由于小程序暂时不支持自定义组件,相关的插件也很少,因此代码写的很烂(这完全是由于我的水平不够导致的)。最近回过头整理了一下相关的开发文档,决定尝试实现小程序的两个功能:前端路由拦截器自定义组件,也算是对最近学习Vue的一点扩展(脑残粉/斜眼)。

<!--more-->

我目前暂时还没有去了解Vue组件的实现原理,但是组件用起来很爽啊!可惜微信小程序处理本身提供的一些组件,并没有提供自定义组件的实现。 下面让我们尝试着实现微信小程序的自定义组件,整个项目地址传送门

1. 原理

小程序使用Page() 函数用来注册一个页面,页面的初始数据、生命周期函数、事件处理函数等都挂在载Page()函数的参数上。因此为了实现组件,我们也必须将组件的属性和方法挂载到当前page对象上。

1.1. wxml

使用template标签声明模板,然后传入data,渲染出页面结构。这里的一个问题是某些组件可能需要动态插入内容,比如tab选项卡面板

1.2. wxss

样式表的管理就比较方便了,可以直接使用@import引入样式表。遇见的问题

  • 字体图标。由于不能直接使用字体资源,可以将字体转为base64格式然后使用,为了方便我直接使用了font-awsome
  • wxss不支持scss语法,这个配置一下gulp就好了,将scss文件编译输出wxss文件。
  • 样式命名依旧参照BEM进行管理

1.3. javascript

组件上需要封装一些数据和方法,由于小程序的数据和方法都是挂载到Page对象上的,可以使用因此需要合并传入的参数,并改变组件方法的this指向。 此外不同的组件需要考虑命名冲突:

  • 为每个组件定义一个特定的name属性,作为整个组件的命名空间
  • 变量的作用域可以使用对象进行封装,如$tab.active,$tab.index
  • 事件处理函数只能注册在Page上,因此使用"$tab.click":()=>{}的方式处理,由于bindtap等事件绑定方法支持{{handleName}}变量解析,因此可以通过为模板传入变量名来调用组件中的方法,这意味着需要将方法名也保存在变量的作用域中

1.4. 挂载属性和方法

这里实现了一个注册组件到page对象上的类,将数据和方法合并到page对象上的工作都是在Component内部实现的: 上面描述了基本原理,大致思路是通过循环和遍历组件的datamethods属性,将组件的方法挂载到page.data.$test下,将组件的方法使用page["$test.click"]的形式注册,并将方法名同时挂在到page.data.$test下。

class Component {
    constructor(options = {}){
        this.options = options;
        this.page = null;

        this.init();
    }
    init(){
        this.setPage();

        this.mergeData();
        this.mergeMethods();
    }

    setPage(){
        let pages = getCurrentPages(),
            len = pages.length;

        this.page = pages[len - 1];
    }

    setData(data){
        this.page.setData(data);
    }

    getData(name){
        return this.page.data[name];
    }

    mergeData(){
        let { name, data } = this.options;

        // 在page.data定义的[name]值会覆盖组件的默认值
        let pageData = this.getData(name);
        this.setData({
            [name]: Object.assign(data, pageData)
        })
    }
    mergeMethods(){
        let { name, methods } = this.options;

        for (let key in methods){
            let method = methods[key];
            if (methods.hasOwnProperty(key) && typeof method === "function") {

                // 在页面上注册方法
                this.page[`${name}.${key}`] = ()=>{
                    let data = this.getData(name);
                    method.call(data);

                    // 同步方法,更新page数据,这里可能有BUG
                    this.setData({
                        [name]: data
                    });
                };

                // 将方法名同步至 page.data上面,实现模板上注册事件
                this.setData({
                    [`${name}.${key}`]: `${name}.${key}`,
                })
            }
        }
    }
}

2. 使用方式

完成了Component的实现,但我们应该如何实现一个组件呢?所有组件都由Component类加工并挂在到page对象上,定义工作分为两步

2.1. 注册

组件的具体属性包括:

  • 命名空间
  • 数据
  • 方法
  • 外部接口

下面是实现一个简单的计数器,是不是看见了Vue的影子呢。

import Component from "../component"

export default {
    name: '$counter',
    data: {
        count: 1,
    },
    methods:{
        click(page){
            this.count++;
        }
    },
    init(name){
        this.name = name;
        new Component(this);
    }
}

模板

<template name="counter">
    <view>{{count}}</view>
    <view>{{msg}}</view>
    <button bindtap="{{click}}">click</button>
</template>

2.2. 导出引用

在对应的页面引入对应组件,并对组件进行初始化

  • pageName.js引入组件数据
  • pageName.wxml引入组件模板
  • app.wxss引入全局组件样式表

2.3. 引入数据

可以在page对象上为组件添加新的属性,同上各组件使用各自的命名空间进行区分

import { $counter }from "../../components/index"
Page({
    data:{
        $counter1: {
            msg: "hello world"
        },
    },
    onLoad(){
        // 多个组件根据init参数名作为命名空间
        let $counter1 = Object.create($counter);
        $counter1.init("$counter1");
    },
});

2.4. 引入模板

模板的数据只能通过data传入,正在尝试为模板添加slot的功能,具体怎么实现还没有思路,限制太多了的感觉。

<import src="../../components/counter/counter.wxml"/>
<template is="counter" data="{{...$counter1}}"/>

3. 目录管理

微信小程序除了最基本的配置之外,对目录并没有什么要求。下面是我 在开发中总结的目录管理。

/components/componentsName下定义组件,组件由下面三部分组成

  • *.js对应组件的数据和方法,建议放在componentsName目录下
  • *.wxml对应组件的模板,建议放在componentsName目录下
  • *.wxss对应组件的样式,由于使用scss进行样式编写,因此组件样式建议放在/styles/components/下,使用componentsName进行区分

组件建立好之后,统一由/components/index.js导出,方便管理。

import $counter from "counter/counter"
export {
    $counter,
}

实际上,在一番折腾之后,我在开发小程序时采用下面的目录层次进行文件管理

wxdemo/ 
    |-assets/
        |- img/
        |- fonts/
        |- ...
    |-api/
        |- _config.js
        |- goods.js
        |- ...
    |-components/
        |- component.js
        |- index.js
        |- components01/
            |- components01.js
            |- components01.wxml
        |- components02/
        |- ...
    |-styles/
        |- main.scss
        |- main.wxss
        |- base
            |- _reset.scss
            |- _color.scss
    |-utils/
        |- wxPromise.js
        |- ...
    |-pages/
        |- start
            |- index.js
            |- index.json
            |- index.wxml
            |- index.wxss
        |- ...
  |-app.js
  |-app.json
  |-app.wxss
  |-...

4. 小结

就这样,一个基本的自定义组件就实现了。实现自定义组件的目的主要是为了减少页面上的重复代码,但是官方并没有提供对应的接口,这是一个十分蛋疼的事,但是我们实现的这个自定义路由还有很多问题,暂时我是不太敢用在实际项目中(我甚至不太喜欢去开发微信小程序~),主要还是用来学习吧。

《同构JavaScript应用开发》读书笔记 实现一个简单的Promise