浏览代码

feat: 新增 core/emitter 订阅发布基础包

zhangyuhan 1 年之前
父节点
当前提交
bce569cb47

+ 1 - 0
core/emitter/.gitignore

@@ -0,0 +1 @@
+coverage/

+ 5 - 0
core/emitter/.prettierignore

@@ -0,0 +1,5 @@
+dist/
+temp/
+coverage/
+pnpm-lock.yaml
+pnpm-workspace.yaml

+ 20 - 0
core/emitter/.prettierrc.json

@@ -0,0 +1,20 @@
+{
+  "semi": false,
+  "singleQuote": true,
+  "trailingComma": "none",
+  "overrides": [
+    {
+      "files": ["*.json5"],
+      "options": {
+        "singleQuote": false,
+        "quoteProps": "preserve"
+      }
+    },
+    {
+      "files": ["*.yml"],
+      "options": {
+        "singleQuote": false
+      }
+    }
+  ]
+}

+ 129 - 0
core/emitter/README.md

@@ -0,0 +1,129 @@
+# Emitter
+
+> 订阅/发布 基础工具包
+
+| Statements                                                                                 | Branches                                                                              | Functions                                                                                | Lines                                                                            |
+| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
+| ![Statements](https://img.shields.io/badge/statements-91.78%25-brightgreen.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-94.5%25-brightgreen.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-94.33%25-brightgreen.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-91.78%25-brightgreen.svg?style=flat) |
+
+### API
+
+| Fn    | 参数                     | 说明       |
+| ----- | ------------------------ | ---------- |
+| $emit | (type, data, expands)    | 发布       |
+| $on   | (type, handler, expands) | 订阅       |
+| $once | (type, handler, expands) | 仅订阅单次 |
+| $off  | (type, handler, expands) | 取消订阅   |
+
+#### 参数
+
+| 参数                    | 类型                           | 说明                                       |
+| ----------------------- | ------------------------------ | ------------------------------------------ |
+| type                    | String                         | 事件标识                                   |
+| data                    | Any                            | 事件数据                                   |
+| handler                 | Function (data, expands, type) | 订阅回调函数                               |
+| expands                 | Object                         | 扩展参数                                   |
+| expands.cross           | Boolean                        | 是否发布跨标签页事件                       |
+| expands.fromCross       | Boolean                        | 是否来自跨标签页事件                       |
+| expands.replay          | Boolean                        | 是否发布可回放事件(仅回放最近一次)       |
+| expands.fromReplay      | Boolean                        | 是否来自可回放事件                         |
+| expands.customEvent     | Boolean                        | 是否发布自定义事件(custom-event-emitter) |
+| expands.fromCustomEvent | Boolean                        | 是否来自自定义事件(custom-event-emitter) |
+
+### 示例
+
+> 更多示例请参考 `test` 目录下的测试用例
+
+```javascript
+this.$on('init', () => {
+  // init
+})
+
+this.$emit('init')
+```
+
+#### 跨标签页通信
+
+> 跨标签页通信,需要在发布事件时设置 `cross` 参数为 `true`
+
+⚠️ 注意:会同时触发本页面的 $on() 监听及其他页面的 $on() 监听,如需不触发本页面的 $on() 监听,请在 $on 回调中单独判断 expands.`fromCross`
+
+```javascript
+this.$on('init', (data, expands) => {
+  // 是否来自跨标签页事件
+  if (expands?.fromCross) {
+  }
+})
+
+this.$emit('init', {
+  cross: true
+})
+```
+
+#### 可回放事件
+
+> 可回放事件,需要在发布事件时设置 `replay` 参数为 `true`
+
+```javascript
+const emitter = new Emitter({
+  replay: true,
+  logger: false,
+  cross: false,
+  customEvent: false
+})
+const testContent = 'test-replay'
+let result = null
+
+emitter.$emit(testContent, testContent, { replay: true })
+emitter.$on(testContent, (event, expands) => {
+  result = event
+  // expect(expands.replay).toBe(true)
+  // expect(expands.fromReplay).toBe(true)
+})
+// expect(result).toBe(testContent)
+emitter.$off(testContent)
+```
+
+#### 自定义事件
+
+> 自定义事件,需要在发布事件时设置 `customEvent` 参数为 `true`
+
+默认用于 addListener, dispatchEvent 通信的event: ** custom-event-emitter **
+
+⚠️ 注意:会同时触发本页面的 $on() 监听,如需不触发本页面的 $on() 监听,请在 $on 回调中单独判断 expands.`fromCustomEvent`
+
+```javascript
+const emitter = new Emitter({
+  replay: false,
+  logger: false,
+  cross: false,
+  customEvent: true
+})
+const testContent = 'test-custom-event-dispatchEvent'
+
+emitter.$on(testContent, (event, expands, type) => {
+  // test('应该正常获取 event, type', () => {
+  //      expect(type).toBe(testContent)
+  //      expect(event).toBe(testContent)
+  //  })
+  //  test('[!] expands.customEvent === undefined', () => {
+  //      expect(expands.customEvent).toBe(undefined)
+  //  })
+  //
+  //  test('[*] expands.fromCustomEvent === true', () => {
+  //      expect(expands.fromCustomEvent).toBe(true)
+  //  })
+})
+
+window.dispatchEvent(
+  new CustomEvent(realEventName, {
+    detail: {
+      type: testContent,
+      event: testContent,
+      expands: {}
+    }
+  })
+)
+
+emitter.$off(testContent)
+```

+ 5 - 0
core/emitter/index.d.ts

@@ -0,0 +1,5 @@
+import { EmitterCoreInstance, EmitterOptions } from './types/emitter'
+
+declare function Emitter(options?: EmitterOptions): EmitterCoreInstance;
+
+export default Emitter;

+ 12 - 0
core/emitter/index.html

@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Example Emitter</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 141 - 0
core/emitter/lib/core/emit/core.ts

@@ -0,0 +1,141 @@
+import {
+  EventHandler,
+  EmitterContext,
+  EmitterCoreInstance,
+  EmitterCoreOptions
+} from '../../../types/emitter'
+import Plugin from '../plugin'
+
+/**
+ * 事件处理器核心类
+ */
+export default class EmitterCore {
+  private all: Map<string, EventHandler[]>
+  private readonly context: EmitterContext
+  private readonly plugins: Plugin
+
+  constructor({ plugins = [] }: EmitterCoreOptions = {}) {
+    this.all = new Map()
+    this.context = {
+      $emit: this.emit.bind(this),
+      $on: this.on.bind(this),
+      $once: this.once.bind(this),
+      $off: this.off.bind(this)
+    }
+    this.plugins = new Plugin({
+      hooks: ['on', 'once', 'off', 'emit'],
+      plugins,
+      context: this.context
+    })
+  }
+
+  getInstance(): EmitterCoreInstance {
+    const instance = {
+      ...this.context,
+      $plugins: this.plugins.space,
+      use: this.plugins.use.bind(this.plugins)
+    }
+
+    return instance
+  }
+
+  /**
+   * 触发钩子事件
+   * @param {string} type - 事件类型
+   * @param {any} args - 事件数据
+   */
+  private _inject(type: string, ...args: any[]): void {
+    this.plugins.apply(type)(...args)
+  }
+
+  /**
+   * 订阅事件
+   * @param {string} type - 事件类型
+   * @param {Function} handler - 事件处理函数
+   * @returns {EmitterContext} - 返回当前实例,支持链式调用
+   */
+  on(type: string, handler: EventHandler, expand: any = {}): EmitterContext {
+    this._inject('on', type, handler, expand)
+    let handlers = this.all.get(type)
+    if (handlers) {
+      handlers.push(handler)
+    } else {
+      this.all.set(type, [handler])
+    }
+    return this.context
+  }
+
+  /**
+   * 订阅一次性事件
+   * @param {string} type - 事件类型
+   * @param {Function} handler - 事件处理函数
+   * @returns {EmitterContext} - 返回当前实例,支持链式调用
+   */
+  once(type: string, handler: EventHandler, expand: any = {}): EmitterContext {
+    this._inject('once', type, handler, expand)
+    const fn: EventHandler = (...args) => {
+      handler(...args)
+      this.off(type, fn, expand)
+    }
+
+    return this.on(type, fn, expand)
+  }
+
+  /**
+   * 取消订阅事件
+   * @param {string} type - 事件类型
+   * @param {Function} handler - 事件处理函数(可选)
+   * @returns {EmitterContext} - 返回当前实例,支持链式调用
+   */
+  off(type: string, handler?: EventHandler, expand: any = {}): EmitterContext {
+    this._inject('off', type, handler, expand)
+    let handlers = this.all.get(type)
+    if (handlers) {
+      if (handler) {
+        const index = handlers.indexOf(handler)
+        if (index !== -1) {
+          handlers.splice(index, 1)
+        }
+      } else {
+        this.all.set(type, [])
+      }
+    }
+    return this.context
+  }
+
+  /**
+   * 发布事件
+   * @param {string} type - 事件类型
+   * @param {*} event - 事件数据
+   * @returns {EmitterContext} - 返回当前实例,支持链式调用
+   */
+  emit(type: string, event: any, expands: any = {}): EmitterContext {
+    if (type !== '*') {
+      const expand = Object.assign(
+        {
+          originEventName: type
+        },
+        expands
+      )
+      this.emit('*', event, expand)
+    }
+
+    this._inject('emit', type, event, expands)
+    this._call(type, event, expands)
+    return this.context
+  }
+
+  /**
+   * 调用事件处理函数
+   * @param {string} type - 事件类型
+   * @param {*} event - 事件数据
+   */
+  private _call(type: string, event: any, expands: any): void {
+    let handlers = this.all.get(type)
+    if (handlers) {
+      handlers.slice().forEach((handler) => {
+        handler(event, expands, type)
+      })
+    }
+  }
+}

+ 14 - 0
core/emitter/lib/core/emit/index.ts

@@ -0,0 +1,14 @@
+import EmitterCore from './core'
+import { EmitterCoreInstance, EmitterOptions } from '../../../types/emitter'
+import { addDefaultPlugins } from './plugins'
+export default function Emitter(
+  options: EmitterOptions = {}
+): EmitterCoreInstance {
+  const { plugins = [] } = options
+
+  const emitter = new EmitterCore({
+    plugins: addDefaultPlugins(options).concat(plugins)
+  }).getInstance()
+
+  return emitter
+}

+ 115 - 0
core/emitter/lib/core/emit/plugins/cross.ts

@@ -0,0 +1,115 @@
+import { EmitCrossPlugin } from '../../../../types/emitter'
+
+interface EmitCrossOptions {
+  useBroad?: boolean
+  name?: string
+}
+
+interface EmitParams {
+  type: string
+  event: any
+  expands?: { cross: boolean; fromCross: boolean }
+}
+
+export default class EmitCross {
+  private _useBroad: boolean
+  private _name: string
+  private _emitter: any // Update the type based on your actual usage
+  private _channel: BroadcastChannel | null
+
+  constructor(options: EmitCrossOptions = {}) {
+    const { useBroad = true, name = 'default' } = options
+    this._useBroad = useBroad
+    this._name = 'cross-share-event--' + name
+    this._emitter = null
+    this._channel = null
+    this.listener()
+  }
+
+  usePlugin(): EmitCrossPlugin {
+    const _this = this
+    return {
+      name: 'cross',
+      use(context: any): void {
+        _this._emitter = context
+      },
+      emit(
+        type: string,
+        event: any,
+        expands: { cross: boolean; fromCross: boolean } = {
+          cross: false,
+          fromCross: false
+        }
+      ): void {
+        if (expands?.cross && !expands?.fromCross) {
+          _this.emit(type, event, expands)
+        }
+      }
+    }
+  }
+
+  checkUseBroad(): boolean {
+    return this._useBroad && typeof window.BroadcastChannel !== 'undefined'
+  }
+
+  setupBroadcastChannel(): void {
+    this._channel = new BroadcastChannel(this._name)
+    this._channel.addEventListener('message', (event) => {
+      const { type, event: payload, expands } = event.data
+      this.on(type, payload, expands)
+    })
+  }
+
+  setupLocalStorage(): void {
+    window.addEventListener('storage', (event) => {
+      if (event.key === this._name && event.newValue) {
+        const { type, event: payload, expands } = JSON.parse(event.newValue)
+        this.on(type, payload, expands)
+      }
+    })
+  }
+
+  on(
+    type: string,
+    event: any,
+    expands: { originEventName?: string; fromCross?: boolean } = {}
+  ): void {
+    if (type === '*' && expands?.originEventName !== '*') {
+      return
+    }
+    if (type) {
+      expands.fromCross = true
+      this._emitter.$emit(type, event, expands)
+    }
+  }
+
+  emit(
+    type: string,
+    event: any,
+    expands: { cross: boolean; fromCross: boolean } = {
+      cross: false,
+      fromCross: false
+    }
+  ): void {
+    const params: EmitParams = {
+      type,
+      event,
+      expands
+    }
+    if (this.checkUseBroad()) {
+      this._channel?.postMessage(params)
+    } else {
+      const data = JSON.stringify(params)
+      localStorage.setItem(this._name, data)
+      localStorage.removeItem(this._name)
+    }
+  }
+
+  listener(): void {
+    if (this.checkUseBroad()) {
+      this.setupBroadcastChannel()
+    } else {
+      this.setupLocalStorage()
+    }
+  }
+}

+ 87 - 0
core/emitter/lib/core/emit/plugins/customEvent.ts

@@ -0,0 +1,87 @@
+import { EmitterCustomEventPlugin } from '../../../../types/emitter'
+
+interface EmitterCustomEventOptions {
+  name?: string
+}
+
+interface EmitterCustomEventParams {
+  type: string
+  event: any
+  expands?: { customEvent: boolean; fromCustomEvent: boolean }
+}
+
+export default class EmitterCustomEvent {
+  private _name: string
+  private _emitter: any
+
+  constructor(options: EmitterCustomEventOptions = {}) {
+    const { name = 'custom-event-emitter' } = options
+    this._name = name
+    this._emitter = null
+    this.listener()
+  }
+
+  usePlugin(): EmitterCustomEventPlugin {
+    const _this = this
+    return {
+      name: 'customEvent',
+      use(context: any): void {
+        _this._emitter = context
+      },
+      emit(
+        type: string,
+        event: any,
+        expands: { customEvent: boolean; fromCustomEvent: boolean } = {
+          customEvent: false,
+          fromCustomEvent: false
+        }
+      ): void {
+        if (expands?.customEvent && !expands?.fromCustomEvent) {
+          _this.emit(type, event, expands)
+        }
+      }
+    }
+  }
+
+  emit(
+    type: string,
+    event: any,
+    expands: { customEvent: boolean; fromCustomEvent: boolean }
+  ): void {
+    const eventData: EmitterCustomEventParams = {
+      type,
+      event,
+      expands
+    }
+    return this.dispatch(this._name, eventData)
+  }
+
+  on(type: string, event: any, expands: { fromCustomEvent: boolean }): void {
+    if (type) {
+      expands.fromCustomEvent = true
+      this._emitter.$emit(type, event, expands)
+    }
+  }
+
+  listener(): void {
+    window.addEventListener(this._name, (e) => {
+      const { type, event, expands } = (e as CustomEvent).detail
+      if (!expands?.customEvent) {
+        this.on(type, event, expands)
+      }
+    })
+  }
+
+  dispatch(eventName: string, eventData: EmitterCustomEventParams): void {
+    window.dispatchEvent(this.createEvent(eventName, eventData))
+  }
+
+  createEvent(
+    eventName: string,
+    eventData: EmitterCustomEventParams
+  ): CustomEvent {
+    return new CustomEvent(eventName, {
+      detail: eventData
+    })
+  }
+}

+ 31 - 0
core/emitter/lib/core/emit/plugins/index.ts

@@ -0,0 +1,31 @@
+import useEmitterLogger from './logger'
+import EmitCross from './cross'
+import EmitterCustomEvent from './customEvent'
+import EmitterKeepReplay from './keepReplay'
+import { EmitterOptions } from '../../../../types/emitter'
+import { PluginInterface } from '../../../../types/plugin'
+
+export function addDefaultPlugins(options: EmitterOptions): PluginInterface[] {
+  const {
+    logger = true,
+    cross = true,
+    customEvent = false,
+    replay = true,
+    name = 'default'
+  } = options
+  const defaultPlugins: PluginInterface[] = []
+
+  if (logger) {
+    defaultPlugins.push(useEmitterLogger(name))
+  }
+  if (cross) {
+    defaultPlugins.push(new EmitCross().usePlugin())
+  }
+  if (customEvent) {
+    defaultPlugins.push(new EmitterCustomEvent().usePlugin())
+  }
+  if (replay) {
+    defaultPlugins.push(new EmitterKeepReplay().usePlugin())
+  }
+  return defaultPlugins
+}

+ 68 - 0
core/emitter/lib/core/emit/plugins/keepReplay.ts

@@ -0,0 +1,68 @@
+import { EmitterKeepReplayPlugin } from '../../../../types/emitter'
+export default class EmitterKeepReplay {
+  private keepCacheKeys: { [key: string]: boolean }
+  private keepCacheMaps: {
+    [key: string]: {
+      event: any
+      expand: { replay?: boolean; fromReplay?: boolean }
+    }
+  }
+  _emitter: any
+
+  constructor() {
+    this.keepCacheKeys = {}
+    this.keepCacheMaps = {}
+    this._emitter = null
+  }
+
+  usePlugin(): EmitterKeepReplayPlugin {
+    const _this = this
+    return {
+      name: 'replay',
+      use(context: any): void {
+        _this._emitter = context
+      },
+      emit(
+        type: string,
+        event: any,
+        expands: { replay: boolean } = { replay: false }
+      ): void {
+        if (expands?.replay) {
+          _this.emit(type, event, expands)
+        }
+      },
+      on(
+        type: string,
+        handler: (event: any, expands?: { fromReplay?: boolean }) => void,
+        expands: any
+      ): void {
+        _this.on(type, handler, expands)
+      }
+    }
+  }
+
+  emit(type: string, event: any, expands: { replay: boolean }): void {
+    if (type !== '*') {
+      this.keepCacheKeys[type] = true
+      this.keepCacheMaps[type] = {
+        event,
+        expand: expands
+      }
+    }
+  }
+
+  on(
+    type: string,
+    handler: (event: any, expands?: { fromReplay?: boolean }) => void,
+    expands: any
+  ): void {
+    const hasCache = this.keepCacheKeys[type] === true
+    const hasReplay = Object.prototype.hasOwnProperty.call(expands, 'replay')
+    const canReplay = hasReplay ? expands.replay : true
+    if (hasCache && canReplay) {
+      const { event, expand } = this.keepCacheMaps[type]
+      expand.fromReplay = true
+      handler(event, expand)
+    }
+  }
+}

+ 47 - 0
core/emitter/lib/core/emit/plugins/logger.ts

@@ -0,0 +1,47 @@
+interface EmitterLogger {
+  name: string
+  use(): void
+  on(type: string): void
+  once(type: string): void
+  off(type: string): void
+  emit(type: string, event: any, expand: any): void
+}
+
+export default function useEmitterLogger(moduleName = ''): EmitterLogger {
+  const colorPrefix = {
+    use: '\x1b[33m[use]\x1b[0m',
+    on: '\x1b[34m[on]\x1b[0m',
+    once: '\x1b[35m[once]\x1b[0m',
+    off: '\x1b[31m[off]\x1b[0m',
+    emit: '\x1b[32m[emit]\x1b[0m'
+  }
+
+  return {
+    name: 'logger',
+    use(): void {
+      console.log(`------> ${moduleName} ${colorPrefix.use} install use`)
+    },
+    on(type: string): void {
+      console.log(
+        `------> ${moduleName} ${colorPrefix.on} \x1b[36m${type}\x1b[0m`
+      )
+    },
+    once(type: string): void {
+      console.log(
+        `------> ${moduleName} ${colorPrefix.once} \x1b[36m${type}\x1b[0m`
+      )
+    },
+    off(type: string): void {
+      console.log(
+        `------> ${moduleName} ${colorPrefix.off} \x1b[36m${type}\x1b[0m`
+      )
+    },
+    emit(type: string, event: any, expand: any): void {
+      console.groupCollapsed(
+        `------> ${moduleName} ${colorPrefix.emit} \x1b[36m${type}\x1b[0m`
+      )
+      console.log(type, event, expand)
+      console.groupEnd()
+    }
+  }
+}

+ 1 - 0
core/emitter/lib/core/index.ts

@@ -0,0 +1 @@
+export * from './emit/index'

+ 69 - 0
core/emitter/lib/core/plugin.ts

@@ -0,0 +1,69 @@
+import {
+  PluginOptions,
+  PluginInterface,
+  PluginHooks,
+  PluginSpace
+} from '../../types/plugin'
+
+// 插件类
+export default class Plugin {
+  context: any
+  space: PluginSpace
+  hooks: PluginHooks
+
+  // 构造函数
+  constructor({ hooks = [], plugins = [], context = null }: PluginOptions) {
+    this.context = context
+    this.space = {}
+    this.hooks = hooks.reduce((m, key) => {
+      m[key] = []
+      return m
+    }, {} as PluginHooks)
+
+    if (plugins.length) {
+      this.install(plugins as PluginInterface[])
+    }
+  }
+
+  // 安装插件
+  install(plugins: PluginInterface[]) {
+    plugins.forEach((plugin) => this.use(plugin))
+  }
+
+  // 使用插件
+  use(plugin: PluginInterface) {
+    if (plugin.name && !this.space[plugin.name]) {
+      this.space[plugin.name] = plugin
+    } else if (plugin.name) {
+      throw new Error(`Plugin ${plugin.name} 已存在。`)
+    }
+
+    const { hooks } = this
+    for (const key in plugin) {
+      if (hooks[key]) {
+        hooks[key].push(plugin[key])
+      }
+    }
+    if (typeof plugin.use === 'function') {
+      plugin.use(this.context)
+    }
+  }
+
+  // 应用钩子函数
+  apply(key: string, handler?: Function) {
+    const fns = this.get(key)
+    return (...args: any[]) => {
+      if (fns.length) {
+        fns.forEach((fn) => fn(...args))
+      } else if (typeof handler === 'function') {
+        handler(...args)
+      }
+    }
+  }
+
+  // 获取钩子函数列表
+  get(key: string) {
+    const { hooks } = this
+    return hooks[key] || []
+  }
+}

+ 2 - 0
core/emitter/lib/main.ts

@@ -0,0 +1,2 @@
+import Emitter from './core/emit'
+export default Emitter

+ 39 - 0
core/emitter/package.json

@@ -0,0 +1,39 @@
+{
+  "name": "@jy/emiiter",
+  "private": true,
+  "version": "0.0.2",
+  "type": "module",
+  "files": [
+    "dist",
+    "index.d.ts"
+  ],
+  "main": "./dist/emitter.umd.cjs",
+  "module": "./dist/emitter.js",
+  "types": "./index.d.ts",
+  "exports": {
+    "types": "./index.d.ts",
+    "import": "./dist/emitter.js",
+    "require": "./dist/emitter.umd.cjs"
+  },
+  "scripts": {
+    "preinstall": "npx only-allow pnpm",
+    "format": "prettier --write --cache .",
+    "dev": "vite",
+    "build": "tsc && vite build",
+    "test:all": "npm run coverage && npm run make-badge && npm run test:ui",
+    "test": "vitest",
+    "test:ui": "vitest --ui  --coverage.enabled=true",
+    "coverage": "vitest run --coverage",
+    "make-badge": "istanbul-badges-readme"
+  },
+  "devDependencies": {
+    "@vitest/coverage-v8": "^0.34.6",
+    "@vitest/ui": "^0.34.6",
+    "istanbul-badges-readme": "^1.8.5",
+    "jsdom": "^22.1.0",
+    "prettier": "^3.1.0",
+    "typescript": "^5.2.2",
+    "vite": "^5.0.0",
+    "vitest": "^0.34.6"
+  }
+}

+ 28 - 0
core/emitter/src/main.ts

@@ -0,0 +1,28 @@
+import Emitter from '../lib/main'
+import { EmitterCoreInstance } from '../types/emitter'
+
+declare global {
+  interface Window {
+    $emitter: EmitterCoreInstance // Update the type based on your Emitter class
+  }
+}
+
+window.$emitter = Emitter()
+window.$emitter.$emit('init')
+window.$emitter.$on('click-button', (...args) => {
+  console.log('click-button', ...args)
+})
+
+document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
+  <div>
+    <h1>Vite + TypeScript</h1>
+    <div class="card">
+      <button id="counter" type="button" onclick="$emitter.$emit('click-button', { step: 1 })">Click-Button</button>
+      <button id="counter" type="button" onclick="$emitter.$emit('click-button', { step: 2 }, { cross: true })">Click-Button To Cross</button>
+      <button id="counter" type="button" onclick="$emitter.$emit('click-button', { step: 3 }, { replay: true })">Click-Button To Replay</button>
+    </div>
+    <p class="read-the-docs">
+      Click on the Vite and TypeScript logos to learn more
+    </p>
+  </div>
+`

+ 1 - 0
core/emitter/src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 119 - 0
core/emitter/test/core.test.ts

@@ -0,0 +1,119 @@
+import { describe, expect, test } from 'vitest'
+import EmitterCore from '../lib/core/emit/core'
+
+// 使用 vitest 的 describe 方法定义测试套件
+describe('EmitterCore', () => {
+  const emitter = new EmitterCore().getInstance()
+
+  test('验证 $on $emit 订阅/发布 参数有效性', () => {
+    const handler = (params, expands, type) => {
+      expect(params).toBe(1)
+      expect(expands.test).toBe(1)
+      expect(type).toBe('test')
+    }
+    emitter.$on('test', handler)
+    emitter.$emit('test', 1, {
+      test: 1
+    })
+  })
+
+  test('验证 $on 多次订阅及 $off 取消订阅', () => {
+    let count = 0
+    const handler = (val) => {
+      count += val
+    }
+    emitter.$on('test-on', handler)
+    emitter.$on('test-on', handler)
+    emitter.$emit('test-on', 1)
+    // 1 * 2 = 2
+    expect(count).toBe(2)
+    // 2 + 2 * 2 = 6
+    emitter.$emit('test-on', 2)
+    expect(count).toBe(6)
+
+    emitter.$off('test-on')
+    emitter.$emit('test-on', 2)
+    expect(count).toBe(6)
+  })
+
+  test('验证 $once 单次订阅', () => {
+    let count = 0
+    const handler = () => {
+      count++
+      expect(count).toBe(1)
+    }
+    emitter.$once('test-once', handler)
+    emitter.$emit('test-once')
+    emitter.$emit('test-once')
+  })
+
+  describe('验证 plugins 插件 hooks 冒泡', () => {
+    const triggerHooks = []
+    const testContent = 'test-plugin-hooks'
+    const emitter = new EmitterCore().getInstance()
+
+    test('use', () => {
+      emitter.use({
+        use(context) {
+          triggerHooks.push('use')
+          expect(emitter).toMatchObject(context)
+        },
+        emit(type, event, expands) {
+          triggerHooks.push('emit')
+          if (type === '*') {
+            expect(expands.originEventName).toBe(testContent)
+          } else {
+            expect(type).toBe(testContent)
+          }
+          expect(event).toBe(testContent)
+          expect(expands.name).toBe(testContent)
+        },
+        once(type) {
+          triggerHooks.push('once')
+          expect(type).toBe(testContent)
+        },
+        on(type) {
+          triggerHooks.push('on')
+          expect(type).toBe(testContent)
+        },
+        off(type) {
+          triggerHooks.push('off')
+          expect(type).toBe(testContent)
+        }
+      })
+      expect(triggerHooks).toEqual(['use'])
+    })
+
+    // 应该触发两次 emit [*, test-plugin]
+    test('$emit 应该触发两次 emit [*, test-plugin]', () => {
+      emitter.$emit(testContent, testContent, {
+        name: testContent
+      })
+      expect(triggerHooks).toEqual(['use', 'emit', 'emit'])
+    })
+
+    test('$once 应该触发  [once, on]', () => {
+      // 应该触发 on  [once, on]
+      emitter.$once(testContent)
+      expect(triggerHooks).toEqual(['use', 'emit', 'emit', 'once', 'on'])
+    })
+
+    test('$off', () => {
+      emitter.$off(testContent)
+      expect(triggerHooks).toEqual(['use', 'emit', 'emit', 'once', 'on', 'off'])
+    })
+
+    test('$on', () => {
+      emitter.$on(testContent)
+      expect(triggerHooks).toEqual([
+        'use',
+        'emit',
+        'emit',
+        'once',
+        'on',
+        'off',
+        'on'
+      ])
+    })
+  })
+})

+ 241 - 0
core/emitter/test/index.test.ts

@@ -0,0 +1,241 @@
+import { describe, expect, test, it } from 'vitest'
+import Emitter from '../lib/core/emit'
+
+describe('Emitter', () => {
+  function testOptions(options) {
+    const emitter = Emitter(options)
+
+    for (const key in options) {
+      if (options[key]) {
+        it(`enable options.${key}`, () => {
+          expect(emitter.$plugins).toHaveProperty(key)
+        })
+      } else {
+        it(`no enable options.${key}`, () => {
+          expect(emitter.$plugins).not.toHaveProperty(key)
+        })
+      }
+    }
+  }
+
+  describe('options 配置启用插件验证', () => {
+    testOptions({
+      replay: true,
+      logger: false,
+      cross: true,
+      customEvent: true
+    })
+
+    testOptions({
+      replay: false,
+      logger: true,
+      cross: false,
+      customEvent: false
+    })
+  })
+
+  describe('注册插件验证', () => {
+    it('options.plugins 自定义注册插件验证', () => {
+      const pluginName = 'addPlugin'
+      const emitter = Emitter({
+        plugins: [
+          {
+            name: pluginName,
+            use() {
+              expect(true).toBe(true)
+            }
+          }
+        ],
+        replay: false,
+        logger: false,
+        cross: false,
+        customEvent: false
+      })
+      expect(emitter.$plugins).toHaveProperty(pluginName)
+    })
+
+    it('use 自定义注册插件验证', () => {
+      const pluginName = 'addPlugin'
+      const emitter = Emitter({
+        replay: false,
+        logger: false,
+        cross: false,
+        customEvent: false
+      })
+      emitter.use({
+        name: pluginName,
+        use() {
+          expect(true).toBe(true)
+        }
+      })
+      expect(emitter.$plugins).toHaveProperty(pluginName)
+    })
+
+    it('重复注册插件拦截验证', () => {
+      const emitter = Emitter({
+        replay: true,
+        logger: false,
+        cross: false,
+        customEvent: false
+      })
+      expect(() => {
+        return emitter.use({
+          name: 'replay',
+          use() {
+            expect(false).toBe(true)
+          }
+        })
+      }).toThrowError('Plugin')
+    })
+  })
+
+  describe('插件 replay 功能验证', () => {
+    test('$emit(..., { replay: true }) 重放功能验证', () => {
+      const emitter = Emitter({
+        replay: true,
+        logger: false,
+        cross: false,
+        customEvent: false
+      })
+      const testContent = 'test-replay'
+      let result = null
+
+      emitter.$emit(testContent, testContent, { replay: true })
+      emitter.$on(testContent, (event, expands) => {
+        result = event
+        expect(expands.replay).toBe(true)
+        expect(expands.fromReplay).toBe(true)
+      })
+      expect(result).toBe(testContent)
+      emitter.$off(testContent)
+    })
+
+    describe('$on(..., { replay: false }) 禁用重放功能验证', () => {
+      const emitter = Emitter({
+        replay: true,
+        logger: false,
+        cross: false,
+        customEvent: false
+      })
+      const testContent = 'test-replay'
+      let result = null
+
+      emitter.$emit(testContent, testContent, { replay: true })
+      emitter.$on(
+        testContent,
+        (event, expands = {}) => {
+          result = event
+          expect(expands.replay).not.toBe(true)
+          expect(expands.fromReplay).not.toBe(true)
+        },
+        {
+          replay: false
+        }
+      )
+      it('设置 replay: false 后不触发 handler', () => {
+        expect(result).toBe(null)
+      })
+
+      it('正常触发后续 $emit handler', () => {
+        emitter.$emit(testContent, testContent)
+        expect(result).toBe(testContent)
+      })
+    })
+  })
+
+  describe('插件 customEvent 功能验证', () => {
+    const realEventName = 'custom-event-emitter'
+
+    function commonTest(testContent, event, expands, type) {
+      test('应该正常获取 event, type', () => {
+        expect(type).toBe(testContent)
+        expect(event).toBe(testContent)
+      })
+      test('[*] expands.customEvent === true', () => {
+        expect(expands.customEvent).toBe(true)
+      })
+
+      test('[!] expands.fromCustomEvent === undefined', () => {
+        expect(expands.fromCustomEvent).toBe(undefined)
+      })
+    }
+
+    describe('$emit(..., { customEvent: true }) 发送及 $on 附加参数 功能验证', () => {
+      const emitter = Emitter({
+        replay: false,
+        logger: false,
+        cross: false,
+        customEvent: true
+      })
+      const testContent = 'test-custom-event'
+
+      emitter.$on(testContent, (...args) => {
+        commonTest(testContent, ...args)
+      })
+      emitter.$emit(testContent, testContent, { customEvent: true })
+      emitter.$off(testContent)
+    })
+
+    describe('window.addEventListener 接收 $emit(...) 发送  功能验证', () => {
+      const emitter = Emitter({
+        replay: false,
+        logger: false,
+        cross: false,
+        customEvent: true
+      })
+      const testContent = 'test-custom-event-listeners'
+      let result = 0
+      const handler = (e) => {
+        result++
+        const { type, event, expands } = e.detail
+        if (type !== '*') {
+          commonTest(testContent, event, expands, type)
+        }
+      }
+      window.addEventListener(realEventName, handler)
+
+      emitter.$emit(testContent, testContent, { customEvent: true })
+
+      test('应该触发两次 handler [*, type]', () => {
+        expect(result).toBe(2)
+      })
+      window.removeEventListener(realEventName, handler)
+    })
+
+    describe('window.dispatchEvent 发送 $on(...) 接收  功能验证', () => {
+      const emitter = Emitter({
+        replay: false,
+        logger: false,
+        cross: false,
+        customEvent: true
+      })
+      const testContent = 'test-custom-event-dispatchEvent'
+
+      emitter.$on(testContent, (event, expands, type) => {
+        test('应该正常获取 event, type', () => {
+          expect(type).toBe(testContent)
+          expect(event).toBe(testContent)
+        })
+        test('[!] expands.customEvent === undefined', () => {
+          expect(expands.customEvent).toBe(undefined)
+        })
+
+        test('[*] expands.fromCustomEvent === true', () => {
+          expect(expands.fromCustomEvent).toBe(true)
+        })
+      })
+
+      window.dispatchEvent(
+        new CustomEvent(realEventName, {
+          detail: {
+            type: testContent,
+            event: testContent,
+            expands: {}
+          }
+        })
+      )
+
+      emitter.$off(testContent)
+    })
+  })
+})

+ 11 - 0
core/emitter/test/plugins/cross.test.ts

@@ -0,0 +1,11 @@
+import { describe, it, expect } from 'vitest'
+import EmitCross from '../../lib/core/emit/plugins/customEvent'
+
+describe('EmitCross', () => {
+  it('EmitCross should inject plugin', () => {
+    const plugin = new EmitCross().usePlugin()
+    expect(typeof plugin).toBe('object')
+    expect(typeof plugin.use).toBe('function')
+    expect(typeof plugin.emit).toBe('function')
+  })
+})

+ 11 - 0
core/emitter/test/plugins/customEvent.test.ts

@@ -0,0 +1,11 @@
+import { describe, it, expect } from 'vitest'
+import EmitterCustomEvent from '../../lib/core/emit/plugins/customEvent'
+
+describe('EmitterCustomEvent', () => {
+  it('EmitterCustomEvent should inject plugin', () => {
+    const plugin = new EmitterCustomEvent().usePlugin()
+    expect(typeof plugin).toBe('object')
+    expect(typeof plugin.use).toBe('function')
+    expect(typeof plugin.emit).toBe('function')
+  })
+})

+ 39 - 0
core/emitter/test/plugins/logger.test.ts

@@ -0,0 +1,39 @@
+import { afterAll, describe, it, expect, vi } from 'vitest'
+import useEmitterLogger from '../../lib/core/emit/plugins/logger'
+
+describe('useEmitterLogger', () => {
+  const consoleMock = vi
+    .spyOn(console, 'log')
+    .mockImplementation(() => undefined)
+
+  afterAll(() => {
+    consoleMock.mockRestore()
+  })
+
+  describe('检查调用打印是否正确', () => {
+    const logger = useEmitterLogger('TestModule')
+    it('[use] is print', () => {
+      logger.use()
+      expect(consoleMock).toHaveBeenCalledOnce()
+      expect(consoleMock).toHaveBeenLastCalledWith(
+        '------> TestModule \x1b[33m[use]\x1b[0m install use'
+      )
+    })
+    it('on is called', () => {
+      logger.on('event')
+      expect(consoleMock).toHaveBeenCalled()
+    })
+    it('once is called', () => {
+      logger.once('event')
+      expect(consoleMock).toHaveBeenCalled()
+    })
+    it('off is called', () => {
+      logger.off('event')
+      expect(consoleMock).toHaveBeenCalled()
+    })
+    it('emit is called', () => {
+      logger.emit('event', {}, {})
+      expect(consoleMock).toHaveBeenCalled()
+    })
+  })
+})

+ 136 - 0
core/emitter/test/plugins/plugin.test.ts

@@ -0,0 +1,136 @@
+import { expect, test, describe, it } from 'vitest'
+import Plugin from '../../lib/core/plugin'
+
+describe('Plugin', () => {
+  const testContext = {}
+
+  it('install plugins', () => {
+    const plugin1 = {
+      name: 'p1',
+      use() {},
+      before() {}
+    }
+    const plugin2 = {
+      name: 'p2',
+      use2() {}
+    }
+
+    test('install of options.plugins', () => {
+      const pluginInstance = new Plugin({
+        plugins: [plugin1, plugin2],
+        hooks: ['before'],
+        context: testContext
+      })
+
+      expect(pluginInstance.space.p1).toEqual(plugin1)
+      expect(pluginInstance.space.p2).toEqual(plugin2)
+    })
+
+    test('install of install()', () => {
+      const pluginInstance = new Plugin({
+        hooks: ['before'],
+        context: testContext
+      })
+
+      pluginInstance.install([plugin1, plugin2])
+
+      expect(pluginInstance.space.p1).toEqual(plugin1)
+      expect(pluginInstance.space.p2).toEqual(plugin2)
+    })
+  })
+
+  it('install and use plugin', () => {
+    const pluginInstance = new Plugin({
+      hooks: ['before', 'after'],
+      context: testContext
+    })
+
+    const samplePlugin = {
+      name: 'samplePlugin',
+      use(context) {
+        context.used = true
+      },
+      before() {
+        console.log('Before hook executed')
+      },
+      after() {
+        console.log('After hook executed')
+      }
+    }
+
+    pluginInstance.use(samplePlugin)
+
+    test('检查 space 调用', () => {
+      expect(pluginInstance.space.samplePlugin).toEqual(samplePlugin)
+    })
+
+    test('检查 hooks', () => {
+      expect(pluginInstance.get('before').length).toBe(1)
+    })
+  })
+
+  it('apply hooks', () => {
+    const pluginInstance = new Plugin({
+      hooks: ['customHook'],
+      context: testContext
+    })
+
+    test('apply customHook', () => {
+      const customHandler = (value) => {
+        expect(value).toBe(42)
+      }
+
+      pluginInstance.use({
+        customHook: customHandler
+      })
+      const appliedHandler = pluginInstance.apply('customHook')
+      appliedHandler(42)
+    })
+
+    test('apply not register hooks', () => {
+      let result = null
+      const tempHook = pluginInstance.apply('not-register-hooks', (value) => {
+        expect(value).toBe('not')
+        result = value
+      })
+      tempHook('not')
+      expect(result).toBe('not')
+    })
+
+    test('apply not register hooks function', () => {
+      let result = null
+      const tempHook = pluginInstance.apply('not-register-hooks', 'xxx')
+      expect(tempHook).toBe(null)
+    })
+  })
+
+  it('get hooks', () => {
+    const pluginInstance = new Plugin({
+      hooks: ['getHook'],
+      context: testContext
+    })
+
+    const getHookHandler1 = () => console.log('Get Hook Handler 1')
+    const getHookHandler2 = () => console.log('Get Hook Handler 2')
+
+    pluginInstance.use({
+      getHook: getHookHandler1
+    })
+
+    pluginInstance.use({
+      getHook: getHookHandler2
+    })
+
+    test('get has hooks length', () => {
+      const hooks = pluginInstance.get('getHook')
+
+      expect(hooks.length).toBe(2)
+    })
+
+    test('get not has hooks length', () => {
+      const hooks = pluginInstance.get('getNotHook')
+
+      expect(hooks.length).toEqual([])
+    })
+  })
+})

+ 24 - 0
core/emitter/tsconfig.json

@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "target": "ES2015",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2015", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src"],
+  "types": ["./types/*.d.ts"]
+}

+ 64 - 0
core/emitter/types/emitter.d.ts

@@ -0,0 +1,64 @@
+import { PluginInterface, PluginSpace } from './plugin'
+
+export type EventHandler = (event: any, expands: any, type: string) => void
+
+export type EmitterEventHandler = (
+  type: string,
+  handler: EventHandler,
+  expand?: any
+) => EmitterContext
+export interface EmitterContext {
+  $emit: (type: string, data?: any, expands?: any) => EmitterContext
+  $on: EmitterEventHandler
+  $once: EmitterEventHandler
+  $off: EmitterEventHandler
+}
+
+export interface EmitterCoreOptions {
+  plugins?: PluginInterface[]
+}
+
+export interface EmitterOptions {
+  plugins?: PluginInterface[]
+  logger?: boolean
+  cross?: boolean
+  customEvent?: boolean
+  replay?: boolean
+  name?: string
+}
+
+export interface EmitterCoreInstance extends EmitterContext {
+  $plugins: PluginSpace
+  use: (plugin: PluginInterface) => void
+}
+
+export interface EmitCrossPlugin extends PluginInterface {
+  name: string
+  use(context: any): void
+  emit(
+    type: string,
+    event: any,
+    expands?: { cross: boolean; fromCross: boolean }
+  ): void
+}
+
+export interface EmitterCustomEventPlugin extends PluginInterface {
+  name: string
+  use(context: any): void
+  emit(
+    type: string,
+    event: any,
+    expands?: { customEvent: boolean; fromCustomEvent: boolean }
+  ): void
+}
+
+export interface EmitterKeepReplayPlugin extends PluginInterface {
+  name: string
+  use(context: any): void
+  emit(type: string, event: any, expands?: { replay: boolean }): void
+  on(
+    type: string,
+    handler: (event: any, expands?: { fromReplay?: boolean }) => void,
+    expands: any
+  ): void
+}

+ 19 - 0
core/emitter/types/plugin.d.ts

@@ -0,0 +1,19 @@
+// 定义插件接口
+export interface PluginInterface {
+  name?: string
+  use?: (context: any) => void
+  [key: string]: any
+}
+
+// 插件空间类型
+export type PluginSpace = Record<string, PluginInterface>
+
+// 定义钩子类型
+export type PluginHooks = Record<string, Function[]>
+
+// 定义构造函数参数类型
+export interface PluginOptions {
+  hooks?: string[]
+  plugins?: PluginInterface[]
+  context?: any
+}

+ 11 - 0
core/emitter/vite.config.ts

@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+  build: {
+    lib: {
+      entry: './lib/main.ts',
+      name: 'Emitter',
+      fileName: 'emitter'
+    }
+  }
+})

+ 12 - 0
core/emitter/vitest.config.ts

@@ -0,0 +1,12 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'jsdom',
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html', 'json-summary']
+    }
+  }
+})