# # 创建组件

本节将介绍如何通过 UIExtension 提供的功能来创建自定义组件，并以实现一个 Counter 功能为例来说明 UIExtension 组件的使用方法。

## # 组件的基本结构

一个基本组件通常包括以下部分：

1. 布局模板 (template)：组件的视图部分，用于定义组件的结构和布局，模板写法可以参考 布局模板。
2. 样式 (styles)：组件的样式可以使用 `style` 属性或 `class` 属性来添加，类似于 HTML。使用 `class` 属性添加的样式需要一个单独的 css 文件。
3. 脚本 (scripts)：组件的逻辑部分，UIExtension 组件需要通过集成实现新的组件类来处理组件的行为和交互。
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` 按钮启动示例：

    <style>
        .my-clock {
            font-size: 32px;
            border: 1px solid #ddd;
            padding: .3em;
        }
    </style>
    <html>
        <template id="layout-template-container">
            <webpdf>
                <custom:clock></custom:clock>
                <div class="fv__ui-body">
                    <viewer @touch-to-scroll></viewer>
                </div>
            </webpdf>
        </template>
    </html>
    <script>
        const { PDFUI, SeniorComponentFactory, modular, appearances: { Appearance } } = UIExtension;
        class ClockComponent extends SeniorComponentFactory.createSuperClass({
            template: `
                <div class="my-clock" @var.clock="$component">
                    @{clock.currentTime|timeformat('yyyy-MM-DD HH:mm:ss')}
                </div>
            `
        }) {
            static getName() {
                return 'clock'
            }
            init() {
                super.init();
                this.currentTime = new Date();
            }
            mounted() {
                super.mounted();
                const timmerId = setInterval(() => {
                    this.currentTime = new Date();
                    this.digest();
                }, 1000);
                this.addDestroyHook(() => {
                    // 这一步是为了确保组件被销毁后停止定时器。
                    clearInterval(timmerId);
                });
            }
        }
        modular.module('custom', []).registerComponent(ClockComponent);

        const CustomAppearance = Appearance.extend({
            getLayoutTemplate: function() {
                return document.getElementById('layout-template-container').innerHTML;
            }
        });
        const libPath = window.top.location.origin + '/lib';
        const pdfui = new PDFUI({
                viewerOptions: {
                    libPath: libPath,
                    jr: {
                        licenseSN: licenseSN,
                        licenseKey: licenseKey
                    }
                },
                renderTo: document.body,
                appearance: CustomAppearance
        });
    </script>

运行上面的示例，可以看到一个实时更新的时钟组件，这只是一个简单的组件，但它展示了如何创建和使用一个组件，在组件的 `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`、`mounted` 和 `destroy`。如上面的示例所示，可以通过重载父类的方法来实现 `init` 和 `mounted` 生命周期。`init` 方法在组件构造时被调用，通常用于初始化一些属性。`mounted` 方法在组件插入 DOM 树后被调用，可以用来对自身或子组件执行一些 DOM 操作。`destroy` 方法需要使用 `addDestroyHook` 添加要销毁的任务。这些任务通常都是消除副作用，比如事件注销或清除定时器。您可以参考在 创建一个简单组件 小节中提到的 `ClockComponent`。

## # 组件间通信

我们知道，子组件可以通过触发事件与父组件进行通信，而父组件可以通过调用子组件的方法与其进行通信。但如果组件彼此之间没有父子关系，它们如何进行通信呢？UIExtension 框架还支持简单的注入功能，通过注入单例对象，可以实现任意组件之间的通信。实现注入的方法很简单, 下面以一个计数器功能为例来进行说明：

1. 第一步，创建一个 CounterService 类。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
               };
           }
       }

最终效果如下所示：

    <style>
        .my-clock {
            font-size: 32px;
            border: 1px solid #ddd;
            padding: .3em;
        }
    </style>
    <html>
        <template id="layout-template-container">
            <webpdf>
                <div style="display: flex;">
                    <custom:modify @setter.step="1">Increment 1</custom:modify>
                    <custom:show-count></custom:show-count>
                    <custom:modify @setter.step="-2">Decrement 2</custom:modify>
                </div>
                <div class="fv__ui-body">
                    <viewer @touch-to-scroll></viewer>
                </div>
            </webpdf>
        </template>
    </html>
    <script>
        const { PDFUI, SeniorComponentFactory, modular, appearances: { Appearance } } = UIExtension;
        class CounterService {
            constructor() {
                this.count = 0;
            }
        }
        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
                };
            }
        }
        modular.module('custom', [])
            .registerComponent(ModifyButtonComponent)
            .registerComponent(ShowCountComponent)
            ;

        const CustomAppearance = Appearance.extend({
            getLayoutTemplate: function() {
                return document.getElementById('layout-template-container').innerHTML;
            },
            // 为了方便验证效果，重写此方法可以阻止控件在未打开文档时被禁用。
            disableAll() {}
        });
        const libPath = window.top.location.origin + '/lib';
        const pdfui = new PDFUI({
                viewerOptions: {
                    libPath: libPath,
                    jr: {
                        licenseSN: licenseSN,
                        licenseKey: licenseKey
                    }
                },
                renderTo: document.body,
                appearance: CustomAppearance
        });
    </script>

在上面的示例中，`CounterService` 类被注入到 `ModifyButtonComponent` 和`ShowCountComponent` 组件中。这使得这两个组件能够访问和修改 `CounterService` 实例的 `count` 属性。当点击 `ModifyButtonComponent` 组件时，计数会递增，而 `ShowCountComponent` 组件则显示当前的计数值。

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

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