# 插件与扩展点

重要提醒

  • 扩展点的设计很重要,在插件化开发过程中需要严格按照规范进行设计。
  • 扩展点一旦定义,项目中禁止删除或修改,仅可新增功能点。
  • ⭐️⭐️⭐️⭐️⭐️ 扩展点的层级只能有一层,不可以嵌套的形式进行使用;一旦存在嵌套,其维护和变更成本将呈几何形式上升。

# 简介

应用上是否需要做插件化,首先考虑业务的定制差异,差异的部分需要用插件去实现,其他通用的部分只需要按照之前的方式在主应用中去实现。 而差异的部分与通用部分是如何连接起来,这个关联点就是我们要设置的扩展点。如下图所示:
插件扩展点与插件关系

# 命名规范

【扩展点】 一般按照业务功能模块进行语义化命名,当前应用中必须保持唯一性标识,前缀以当前应用的 服务名称(package.json 的 name) 为首,后缀必须以-epu-epl-ep结尾,其中epu为UI类扩展点(ExtensionPoint for UI),epl为逻辑类扩展点(ExtensionPoint for Logics),-ep为不限制类型扩展点(ExtensionPoint),例如:
首页页头扩展点[服务名称]-home-header-epu
首页页脚扩展点[服务名称]-home-footer-epu
签署前签名逻辑校验扩展点[服务名称]-signpage-presign-checksign-epl
签署前处理扩展点[服务名称]-signpage-presign-ep

【扩展点调用插件的函数名】 驼峰命名法,命名规范为:服务名称 +扩展点名称 + EsExtensionPoint,例如
首页页头扩展点[服务名称] + HomeHeaderEpuEsExtensionPoint
首页页脚扩展点[服务名称] + HomeFooterEpuEsExtensionPoint
签署前校验扩展点[服务名称] + SignpagePresignEpEsExtensionPoint

【插件】
文件命名 参照项目命名规则, 以当前应用的 服务名称(package.json 的 name) 为首, 以-plugin结尾,例:[服务名称]-home-header-plugin
插件目录命名 /plugins/项目(或基线)标识/插件名(或多插件集合(单插件对应多扩展点)目录),例:/plugins/base/[服务名称]-home-header-plugin/plugins/base/[服务名称]-home-header-banner-plugin,如下所示:

--- plugins
    |
    |-- base // 基线目录
    |   |
    |   |-- [服务名称]-home-header-plugin // 插件目录(插件集合名)
    |   |   |
    |   |   |-- src
    |   |       |
    |   |       |-- plugin
    |   |       |   |
    |   |       |   |-- home-header-left-plugin.ts
    |   |       |   |
    |   |       |   |-- home-header-right-plugin.ts
    |   |       |
    |   |       |-- index.ts
    |   |
    |   |-- [服务名称]-home-footer-plugin // 插件目录
    |       |
    |       |-- src
    |           |
    |           |-- plugin  
    |           |
    |           |-- index.ts
    |
    |-- hangzhou // 项目目录
    |   |
    |   |-- [服务名称]-home-header-plugin // 插件目录(插件集合名)
    |   |   |
    |   |   |-- src
    |   |       |
    |   |       |-- plugin
    |   |       |   |
    |   |       |   |-- home-header-left-plugin.ts
    |   |       |   |
    |   |       |   |-- home-header-right-plugin.ts
    |   |       |
    |   |       |-- index.ts
    |   |
    |   |-- [服务名称]-home-footer-plugin // 插件目录
    |       |
    |       |-- src
    |           |
    |           |-- plugin  
    |           |
    |           |-- index.ts
    |
    |-- config.json
    

插件文件中name值 必须确保唯一性,一般按照目录命名短横线拼接页面(功能模块)-...(扩展点拆分粒度自定义)-plugin,例:

export default (): Plugin => ({
  name: 'home-header-baner-plugin', // 插件的名称,唯一标识符
  ...
})

插件下package.json文件中name值 必须确保唯一性,在插件name值(或插件目录名)的基础上加上@插件所属域标识/前缀(当插件可以通过插件平台去管理时,前缀的作用就能体现出来,可以防止插件包名冲突),例:

package.json

{
  "name": "@esign-gov-base/home-header-plugin"
}

{
  "name": "@esign-gov-hangzhou/home-header-plugin"
}

插件中的hooks方法 单个hook方法时须与扩展点名称保持一致,如果需要自定义多个hook方法时命名自定义(需保证唯一性)。

提示

扩展点、插件的业务语义化部分命名可以保持统一,如:扩展点[服务名称]-home-header-epu--- 插件[服务名称]-home-header-plugin,这样可以方便开发过程中或后续项目维护时 扩展点和插件互找。

# 扩展点与插件的实现

# 案例一(以组件的方式实现功能)

若以组件方式实现功能,需保证 插件资源的 key 值, EsExtensionPoint 组件的 name 参数 和 插件的 hook 必须保持一致

# main.js

import { EsExtensionPoint } from '@esign/plugin-core'

Vue.use(EsExtensionPoint, {
  configs,
  useEventEmitter: true
})

# 插件代码

import { createAsyncWaterfall } from '@modern-js/plugin'
import Component from './components/index.tsx'
import type { Plugin, PluginAPI } from '@esign/plugin-core'

export default (): Plugin => ({
  name: 'ui-plugin',
  registerHook: {
    customComponent: createAsyncWaterfall(), // 自定义组件 hooks
  },
  setup({ setAppContext, useAppContext }: PluginAPI) {
    return {
      // 通过自定义hook返回组件
      'ui-epu': () => {
        return Component
      }
    }
  }
})

# 调用插件demo test.vue

<template>
  <div>
    <h1>{{transformTxt}}</h1>
    <!-- 扩展点插件 -->
    <EsExtensionPoint name="ui-epu" msg="Welcome to the Esign Plugin Demo" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component({})
export default class TestView extends Vue {
  async mounted() {
  }
}
</script>

# 案例二(以方法形式实现功能)

# 插件代码

import { createAsyncWaterfall } from '@modern-js/plugin'
import Component from './components/index.tsx'
import type { Plugin, PluginAPI } from '@esign/plugin-core'

export default (): Plugin => ({
  name: 'ui-plugin',
  registerHook: {
    customComponent: createAsyncWaterfall(), // 自定义组件 hooks
  },
  setup({ setAppContext, useAppContext }: PluginAPI) {
    return {
      // 方式二,通过自定义hook返回组件
      'ui-epu': () => {
        return Component
      }
    }
  }
})

# 调用插件demo test.vue

<template>
  <div>
    <h1>{{transformTxt}}</h1>
    <component :is="homePageComponentName" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
// pluginManager 文件夹,与src、plugins同级,作用为维护插件与扩展点的对应关系
import { homePageEpuEsExtensionPoint } from '../../pluginManager'

@Component({})
export default class TestView extends Vue {
  homePageComponentName = ''
  async mounted() {
    // 此为插件引入方法,项目一经发布,不可删除
    this.homePageComponentName = await homePageEpuEsExtensionPoint();
  }
}
</script>

# pluginManager/index.js

import pluginManager, { createExtensionPointHook } from '@esign/plugin-core'

// 首页插件调用方法
// 方法名称一旦确定,不再更改
export async function homePageEpuEsExtensionPoint(name: string) {
  const name = 'home-page-epu'
  const path = getPluginPath(name) // 获取插件路径

  // 插件所提供的hooks,只可新增,不可修改及删除
  const hooks = await pluginManager({ path, hooks: { [name]: createExtensionPointHook() } })
  const [ Component ] = await hooks[name](null)
  Vue.component(Component.name, Component)

  // 返回内容格式可自定义
  return Component.name
}

# 扩展点监控

思路:在主应用编译时按照扩展点的命名规则(-ep/-epu/-epl)进行全局扫描扩展点

# 方案一:插件通过EsExtensionPoint组件的方式引入

数据项 描述
扩展点名称 EsExtensionPoint组件prop中的name值,同时也是config.json中plugins下对应的key,例:home-header-epu
扩展点页面path(项目中的文件目录路径、文件名) EsExtensionPoint组件被引用的页面文件所在路径、页面文件名等,例:/src/views/Home/index.tsx
主应用版本号 主应用中设置的版本号(根据迭代版本规则设置),例:2.5.3
插件路径 插件编译后的config.json中plugins下对应的value,例:/plugins/base/home-header-plugin/index.1.0.1.js

扫描方式:每次主应用构建前通过组件依赖关系收集的webpack插件进行项目全局扫描, 将收集到的页面与组件关系上传服务器进行清洗,组成这样对应关系:扩展点——所在页面——插件——主应用版本号

监控方式:例如:1.0.0版本的应用上定义了一个扩展点home-header-epu ,扩展点所在页面为/src/views/Home/index.tsx文件中,加载的插件为/plugins/base/home-header-plugin/index.1.0.1.js,1.1.0版本在发布前也会生成新的一组对应关系与1.0.0版本进行比对,如果发现扩展点缺失、扩展点所在的页面不存在或对应的插件路径未配置时,立即做告警或终止项目构建处理(但也有例外,如果上一个版本的扩展点在当前版本上缺失且无对应的页面,不做告警或拦截处理,该情况主要是指新版本上移除了一些废弃功能的场景)。

规则:总之,一般情况下之后项目版本升级时不允许修改原有扩展点(仅允许增加新扩展点,不允许修改或删除历史版本设置的扩展点)

# 方案二:插件与扩展点通过方法的形式实现

数据项 描述
扩展点名称 config.json中plugins下对应的key,例:home-header-epu
扩展点对应插件的调用函数 pluginManager/index.ts文件中以EsExtensionPoint结尾的方法
扩展点页面path(项目中的文件目录路径、文件名) 扩展点对应插件的调用函数被调用的页面文件地址,例:/src/views/Home/index.tsx
主应用版本号 主应用中设置的版本号(根据迭代版本规则设置),例:2.5.3
插件路径 插件编译后的config.json中plugins下对应的value,例:/plugins/base/home-header-plugin/index.1.0.1.js

扫描方式:需要实现主应用全局扫描的插件调用方法被调用的页面情况(扫描除pluginManager/index.ts文件之外的文件),将收集到的数据项上传服务器,组成这样的对应关系:扩展点——所在页面——插件调用函数——插件——主应用版本号

监控方式:同方案一,当后续版本对上一个版本的一个或多个数据项有修改,即做告警或终止项目构建处理。

规则:同方案一

上次更新: 9/14/2023, 8:58:06 AM