# 创建组件

本章节介绍了如何通过 UIExtension 提供的能力创建一个自定义组件,并以实现一个 Counter 功能为例子,说明 UIExtension 组件用法。

# 组件的基本结构

一个基本的组件通常由这几个部分组成:

  1. 布局模板(template): 这是组件的视图部分,用于定义组件的结构和布局, 模板写法可以参考这篇文章
  2. 样式(styles): 组件的样式可以通过 style 属性,也可以通过 class 属性添加样式,这和 HTML 写法相同,通过 class 属性添加的样式,需要单独的样式文件;
  3. 脚本(scripts):组件的逻辑部分,UIextension 组件需要通过集成实现新的组件 class 来处理组件的行为和交互。
  4. 模块(module): 如果一个组件要被其他组件引用,则必须为其指定名称,并注册到模块中,关于模块化,可以参考这篇文章

一下是一个组件的基本结构示例:

/* my-component.css */
.my-counter {
    display: flex;
}
// my-component.js

const { SeniorComponentFactory } = UIExtension;
class MyComponent extends SeniorComponentFactory.createSuperClass({
    template: `
        <div class="my-counter" @var.my_counter="$component">
            <button class="my-btn" @on.click="my_counter.increment()">+</button>
            <div class="my-viewer">@{my_counter.count}</div>
            <button class="my-btn" @on.click="my_counter.decrement()">-</button>
        </div>
    `
}) {
    static getName() {}
    init() {
        super.init();
        this.count = 0;
    }
    increment() {
        this.count ++;
        this.digest(); // 数据变更后,必须主动触发更新
    }
    decrement() {
        this.count --;
        this.digest(); // 数据变更后,必须主动触发更新
    }
}
modular.module('custom', []).registerComponent(MyComponent);

# 创建一个简单组件

基于上述对组件基本结构描述,现在我们来创建一个可以显示时钟组件, 请点击下面的 run 按钮,启动示例:

运行上面的示例,可以看到一个实时更新的时钟组件,这只是一个简单的组件,但它展示了如何创建和使用一个组件,以及在组件的init,mounted 声明周期中初始化和运行定时器,以及如何在组件模板中引用组件对象上的属性。你可以这个示例的基础上进一步扩展和自定义。

# 组件的事件触发和绑定

组件不仅可以展示数据,还可以于用户交互, 父组件也可以通过监听子组件的事件进行父子组件间的交互。通过 UIExtension , 我们可以实现事件触发和监听实现交互功能。

# 事件触发

要在组件中触发一个事件,我们可以使用 trigger 方法, 这个方法接受一个事件名称(必传)和多个要传输的数据(可选)。下面是一个示例:

class DidaComponent extends SeniorComponentFactory.createSuperClass({
    template: `
        <div></div>
    `
}) {
    static getName() {
        return 'dida'
    }
    mounted() {
        super.mounted();
        const execute = () => {
            if(this.isDestroyed) {
                return;
            }
            this.trigger('dida', performance.now());
            requestIdleCallback(execute);
        };
        requestIdleCallback(execute);
    }
}
modular.module('custom', []).registerComponent(DidaComponent);

这个例子中, <dida></dida> 组件在空闲时,变回向外触发一个 dida 事件,并传递一个时间。

# 事件监听

要监听一个组件的事件,有两种方式,一种是通过 @on.event-name 指令实现, 另一种是通过 Component#on 接口实现,下面的例子将沿用上述dida组件来展示这两种用法:

class DidaBoxComponent extends SeniorComponentFactory.createSuperClass({
    template: `<div @var.box="$component">
        <custom:dida name="dida" @on.dida="box.onDidaDirectiveEvent($args[0])"></custom:dida>
    </div>
    `
}) {
    static getName() {
        return 'dida-box';
    }
    onDidaDirectiveEvent(time) {
        console.log('执行了通过指令监听的事件', time)
    }
    mounted() {
        super.mounted();
        this.getComponentByName('dida').on('dida', time => {
            console.log('执行了通过on接口监听的事件', time)
        })
    }
}

# 原生DOM事件监听

@on 指令除了可以监听从组件中触发的自定义事件外,还可以监听DOM原生事件, 具体用法可以参考 @on指令 这一章的描述。

# 组件的生命周期

UIextension 组件的生命周期比较简单,常用的有这三个: init, mounteddestroyinitmounted 生命周期如上述几个示例所示,只要重载父类的方法即可,init 会在组件构造时调用,通常用于初始化一些属性, mounted 则是在组件插入到DOM树后调用,在这里可以对自身或子组件进行一些DOM操作, 而 destroy 则是需要通过 addDestroyHook 来添加待销毁的任务,这些任务通常都是消除副作用,比如:事件注销、清除定时器。可以参考在 创建一个简单组件 一节中提到的 ClockComponent

# 组件间通信

我们知道,子组件向父组件通信,可以通过触发事件,而父组件向子组件通信可直接调用其方法,而非父子组件之间如何通信呢。 UIExtension 框架还支持了简单的注入功能,通过注入单例对象,可以实现任意组件之间的通信。实现注入的方法很简单, 下面以一个计数器功能为例:

  1. 第一步,创建一个 CounterService class, CounterService 的作用就是记录一个 count 属性,这个属性值可以被任何组件共享:

    class CounterService {
        constructor() {
            this.count = 0;
        }
    }
    
  2. 第二步,创建两个组件,一个用于改变计数,一个用于显示计数, 这两个组件都注入了 CounterService:

    class ModifyButtonComponent extends SeniorComponentFactory.createSuperClass({
        template: `<button @on.click="$component.onClick()"></button>`
    }) {
        static getName() {
            return 'modify';
        }
        static inject() {
            return {
                service: CounterService
            };
        }
        createDOMElement() {
            return document.createElement('button');
        }
        init() {
            this.step = 0;
        }
        onClick() {
            this.service.count += this.step;
            this.digest();
        }
        setStep(step) {
            this.step = step;
        }
    }
    class ShowCountComponent extends SeniorComponentFactory.createSuperClass({
        template: `<span style="border: 1px solid #ddd;padding: .5em 1em; display: inline-block;">@{$component.service.count}</span>`
    }) {
        static getName() {
            return 'show-count';
        }
        static inject() {
            return {
                service: CounterService
            };
        }
    }
    

现在我们来看最终的效果:

在上面的示例中,CounterService类被注入到ModifyButtonComponentShowCountComponent组件中。这允许两个组件访问和修改CounterService实例的count属性。 ModifyButtonComponent组件在单击时会增加计数,而ShowCountComponent组件则显示当前计数值。

通过将CounterService注入两个组件,它们共享相同的服务实例,从而实现它们之间的通信。对一个组件中的count属性所做的任何更改也将反映在另一组件中。

依赖注入是一项强大的功能,允许组件在不紧密耦合的情况下进行通信和共享数据。它促进应用程序的模块化和可重用性。