Skip to content

创建组件

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

组件的基本结构

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

  1. 布局模板 (template):组件的视图部分,用于定义组件的结构和布局,模板写法可以参考 布局模板
  2. 样式 (styles):组件的样式可以使用 style 属性或 class 属性来添加,类似于 HTML。使用 class 属性添加的样式需要一个单独的 css 文件。
  3. 脚本 (scripts):组件的逻辑部分,UIExtension 组件需要通过集成实现新的组件类来处理组件的行为和交互。
  4. 模块 (Module): 如果一个组件需要被其他组件引用,则必须为其指定名称并在模块中进行注册。有关模块化的更多信息,请参考 模块化

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

css
/* my-component.css */
.my-counter {
    display: flex;
}
javascript
// 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 按钮启动示例:

html

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

运行上面的示例,可以看到一个实时更新的时钟组件,这只是一个简单的组件,但它展示了如何创建和使用一个组件,在组件的 initmounted 生命周期中初始化和运行定时器,以及如何在组件模板中引用组件对象属性。您可以根据您的需求进一步扩展和自定义此示例。

组件的事件触发和绑定

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

事件触发

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

javascript
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 组件来展示这两种用法:

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

组件间通信

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

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

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

    javascript
    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
            };
        }
    }

最终效果如下所示:

html

<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 类被注入到 ModifyButtonComponentShowCountComponent 组件中。这使得这两个组件能够访问和修改 CounterService 实例的 count 属性。当点击 ModifyButtonComponent 组件时,计数会递增,而 ShowCountComponent 组件则显示当前的计数值。

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

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