English | [中文](./plugin_zh.md)
# Plugins
## SDK that supports plugin registration
- `@huolala-tech/page-spy-browser@^1.7.0`;
- `@huolala-tech/page-spy-wechat@^1.7.0`;
- `@huolala-tech/page-spy-uniapp@^1.7.0`;
## Official plugins
- [`@huolala-tech/page-spy-plugin-rrweb`](../packages/page-spy-plugin-rrweb): use `rrweb` to record the DOM mutation;
- [`@huolala-tech/page-spy-plugin-data-harbor`](../packages/page-spy-plugin-data-harbor): providing offline caching and data downloading capabilities;
## Definition of a Plugin
```ts
import { SocketStoreType } from '@huolala-tech/page-spy-types/lib/base';
import { PluginOrder } from '@huolala-tech/page-spy-types';
import { InitConfig } from 'types';
export abstract class PageSpyPlugin {
/**
* Each plugin requires a specified name, serving as the "identity" of the current plugin.
* The functionalities of registering and disabling plugins within PageSpy rely on the name property.
*/
public abstract name: string;
/**
* Specify the plugin ordering. The plugin invocation will be in the following order:
* 1. Plugins with `enforce: "pre"`;
* 2. Plugins without enforce value;
* 3. Plugins with `enforce: "post"`;
*/
public abstract enforce?: PluginOrder;
// Called when `new PageSpy()` is invoked.
public abstract onInit: (params: OnInitParams) => any;
// Called after the rendering of PageSpy is completed (if there is a rendering process).
public abstract onMounted?: (params: OnMountedParams) => any;
// When PageSpy is no longer needed, plugins should have a reset/recovery functionality.
public abstract onReset?: () => any;
}
export interface OnInitParams {
// Merged configuration information about PageSpy instantiation parameters provided by the user.
config: Required;
// Wraps the socket instance, allowing plugin developers to interact with the debugging terminal/API through this property.
socketStore: SocketStoreType;
}
export interface OnMountedParams {
// Root node of the PageSpy rendering
root?: HTMLDivElement;
// Root node of the PageSpy rendering pop-up
content?: HTMLDivElement;
// Wraps the socket instance, allowing plugin developers to interact with the debugging terminal/API through this property.
socketStore: SocketStoreType;
}
```
## Behavioral Conventions
If the current plugin collects (or wishes to publicly expose) some form of "data" regarding platform behavior, in addition to broadcasting data on `socketStore`, we conventionally dispatch an "public-data" internal event on the `socketStore` instance. This is done to meet the needs of plugins that require statistics or persistent data, allowing them to uniformly collect data from this event. If a plugin feels that certain data should not be "publicly" exposed, there is no need to dispatch the "public-data" event.
## Plugin Implementation Example
> [!NOTE]
> The following content is just an example.
Use case: Recording DOM on the client-side using rrweb, including:
- Adding a "Download Recording Data" button in the client-side popup; clicking the button initiates the file download.
- Listening for event messages from the debugging terminal and interacting with the debugging terminal.
```ts
import { record } from 'rrweb';
import { eventWithTime } from '@rrweb/types';
import {
SpyMessage,
PageSpyPlugin,
OnInitParams,
OnMountedParams,
} from '@huolala-tech/page-spy-types';
type Options = Parameters[number];
export default class XXXPlugin implements PageSpyPlugin {
name = 'XXXPlugin';
events: eventWithTime[] = [];
private static hasInited = false;
private static hasMounted = false;
constructor(public options: Options = {}) {}
onInit({ socketStore }: OnInitParams) {
if (XXXPlugin.hasInited) return;
XXXPlugin.hasInited = true;
record({
...this.options,
emit(event) {
// Construct the data to be sent out
const message: SpyMessage.MessageItem = {
type: 'rrweb-event',
role: 'client',
data: event,
};
// Broadcast data through socketStore
socketStore.broadcastMessage(message);
// Simultaneously dispatch the "public-data" event
socketStore.dispatchEvent('public-data', message);
},
});
// Listen for events sent from the debugging terminal
// Assuming "rrweb-cache" is a ws "event" message sent by the debugging terminal
socketStore.addListener('rrweb-cache', ({ source }, reply) => {
// source.data - Parameters sent by the debugging terminal
const params = source.data;
// Plugin processing logic
// const result: SpyMessage.MessageItem = ...
// Call reply(...), and socketStore will be responsible for unicasting the data to the specified debugging terminal
reply(result);
});
}
onMounted({ content, socketStore }: OnMountedParams) {
if (XXXPlugin.hasMounted) return;
XXXPlugin.hasMounted = true;
const recordBtn = document.createElement('div');
recordBtn.id = 'download-rrweb-event';
// "page-spy-content__btn" class can reuse button styles
recordBtn.className = 'page-spy-content__btn';
recordBtn.innerText = 'Download Recording Data';
recordBtn.addEventListener('click', () => {
const data = new Blob([JSON.stringify(this.events)], {
type: 'application/json',
});
const url = URL.createObjectURL(data);
const a = document.createElement('a');
a.download = `${new Date().toLocaleString()}.json`;
a.href = url;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
});
content.appendChild(recordBtn);
}
// When $pageSpy.abort() is called, `onReset()` is triggered
onReset() {
XXXPlugin.hasInited = false;
XXXPlugin.hasMounted = false;
const root = document.getElementById('download-rrweb-event');
if (root) {
root.remove();
}
}
}
```
## Usage of the Plugin
```html
```