# Create-Data-Verge-Widget **Repository Path**: DataVerge/Create-Data-Verge-Widget ## Basic Information - **Project Name**: Create-Data-Verge-Widget - **Description**: Data Verge 数据面板组件插件脚手架。一个插件包只注册一个 Vue 3 + TypeScript 组件。 - **Primary Language**: TypeScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-05-19 - **Last Updated**: 2026-07-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Data Verge Widget 插件开发说明 这是 Data Verge 数据面板组件插件脚手架。一个插件包注册一个组件,主应用负责组件挂载、数据查询、加载状态、错误状态和配置面板;插件端只负责渲染和处理主应用主动推送的成功数据,并在需要时通过受控方法请求主应用刷新或发送事件。 ## 目录说明 ```txt src/ main.ts 插件注册入口 App.vue 插件组件示例 manifest.ts 插件包元信息,构建后生成 dist/manifest.json runtime/vue-renderer.ts Vue 组件到 Data Verge renderer 的适配层 types/context.ts 注册、数据面板、属性面板相关类型 types/property-schema.ts 右侧属性面板配置 types/render.ts 插件渲染上下文和查询结果类型 types/public-props.ts 公共属性类型 public/assets/ 插件静态资源 ``` ## 开发启动 ```sh pnpm install pnpm dev ``` 本地开发时 `src/App.vue` 可以独立预览。接入主应用后,组件由 `src/main.ts` 中注册的 renderer 挂载。 ## 主应用本地联调 插件开发时可以不打包、不上传,直接让主应用从插件端 Vite dev server 加载 `src/main.ts`。 插件端启动: ```sh pnpm dev ``` 模板默认使用固定端口 `5174`,主应用会通过下面的地址加载插件注册入口: ```txt http://localhost:5174/src/main.ts ``` 主应用 `apps/web-antd/.env.widget.debug` 示例: ```env WIDGET_DEBUG_ENABLE=true WIDGET_DEBUG_PLUGIN_KEY=detail-table WIDGET_DEBUG_GROUP_KEY=table WIDGET_DEBUG_ENTRY_URL=http://localhost:5174/src/main.ts WIDGET_DEBUG_BASE_URL=http://localhost:5174/ WIDGET_DEBUG_USE_VITE_CLIENT=true ``` 字段说明: ```txt WIDGET_DEBUG_ENABLE 是否启用本地调试,仅主应用 dev/serve 时生效 WIDGET_DEBUG_PLUGIN_KEY 要被本地入口覆盖的插件 key WIDGET_DEBUG_GROUP_KEY 要被本地入口覆盖的分组 key,可用于避免误命中同名插件 WIDGET_DEBUG_ENTRY_URL 插件端 Vite dev server 暴露的注册入口 WIDGET_DEBUG_BASE_URL 插件端静态资源根地址,通常是 dev server 根地址 WIDGET_DEBUG_USE_VITE_CLIENT 是否让主应用尝试加载插件端 @vite/client ``` 调试流程: ```txt 插件端 pnpm dev -> 主应用开启 .env.widget.debug -> 重启主应用 dev server -> 主应用加载匹配 pluginKey/groupKey 的组件时动态 import 插件端 src/main.ts ``` 如果只修改插件端代码,通常刷新主应用页面即可看到最新效果;不需要执行 `pack:plugin`,也不需要上传插件包。 `src/main.ts` 在开发态仍支持插件端独立预览,但只有浏览器直接打开插件端 dev server 页面时才会自动挂载到 `#app`。被主应用远程加载时,它只会作为插件注册入口导出 `register(ctx)`,不会覆盖主应用页面。 ## 注册组件 插件必须导出 `register(ctx)`,并在里面调用 `ctx.registerWidget()`。 ```ts import Widget from './App.vue'; import { createVueWidgetRenderer } from './runtime/vue-renderer'; export function register(ctx: DataPanelPluginContext) { ctx.registerWidget({ buildQueryParams, dataConfig, defaultProps, defaultPublicProps, interactionConfig, propertySchema, renderer: createVueWidgetRenderer(Widget), }); } ``` 常用注册项: ```ts ctx.registerWidget({ // 查询前参数构建函数,用于把插件端运行时状态转换成分页、参数等查询扩展项 buildQueryParams, // 数据面板配置,决定右侧数据页签有哪些投放区域 dataConfig, // 私有属性默认值,最终会作为 host.props 传给插件 defaultProps, // 公共属性默认值,如标题、尺寸、位置、背景等 defaultPublicProps, // 右侧属性面板配置,建议从 src/types/property-schema.ts 引入 propertySchema, // 插件渲染器,Vue 插件通常使用 createVueWidgetRenderer renderer: createVueWidgetRenderer(Widget), }); ``` ## 插件清单 manifest.json `manifest.json` 是插件包的清单文件,用来让主应用识别这个插件属于哪个分类、插件唯一标识是什么、当前版本是多少。模板中不要直接手写 `dist/manifest.json`,而是在 `src/manifest.ts` 中维护源配置,执行构建时脚本会自动生成 `dist/manifest.json`。 `src/manifest.ts` 示例: ```ts export interface WidgetManifest { groupKey: string; pluginKey: string; version: string; } export const widgetManifest: WidgetManifest = { groupKey: 'table', pluginKey: 'detail-table', version: '1.0.0', }; ``` 构建后生成的 `dist/manifest.json`: ```json { "groupKey": "table", "pluginKey": "detail-table", "version": "1.0.0" } ``` 字段说明: ```txt groupKey 组件分类标识,用于左侧组件菜单分类,也会参与远程插件资源路径组织 pluginKey 插件唯一标识,用于组件注册、渲染、实例保存和后续运行时加载 version 插件版本号,用于发布、升级和缓存识别 ``` 注意事项: - `pluginKey` 发布后不要随意修改,否则历史面板中已保存的组件实例可能无法匹配到新的插件。 - `groupKey` 需要和主应用中已有分类一致;如果服务端或主应用没有对应分类,组件可能会进入兜底分类或无法按预期展示。 - `version` 建议使用语义化版本,例如 `1.0.0`、`1.1.0`、`2.0.0`。 - `pnpm run build` 会在构建结束后执行 `scripts/write-manifest.mjs`,将 `src/manifest.ts` 写入 `dist/manifest.json`。 - `pnpm run pack:plugin` 会把 `dist/manifest.json` 一起打进 `dist.zip`,上传插件包时它必须位于压缩包根目录。 `manifest.json` 只描述插件包身份,不负责声明属性面板、数据面板或渲染器。这些运行时能力仍然在 `src/main.ts` 的 `register(ctx)` 中通过 `ctx.registerWidget()` 注册。 ## 接收数据 插件组件通过 `host` prop 接收主应用推送的上下文,但查询结果不再挂在 `host.data` 上。数据查询一定发生在主应用中,主应用刷新成功后会触发插件端监听的 `EventNames.DataChange` 事件。 ```ts import type { DataPanelQueryResult, DataPanelWidgetHost } from './types/render'; import { EventNames } from './types/render'; const props = defineProps<{ host?: DataPanelWidgetHost; }>(); let unsubscribeDataChange: (() => void) | undefined; onMounted(() => { unsubscribeDataChange = props.host?.on(EventNames.DataChange, (data?: DataPanelQueryResult) => { // 主应用每次刷新成功后都会进入这里 // 可以在这里做字段映射、过滤、排序、聚合、图表或表格更新 }); }); onBeforeUnmount(() => { unsubscribeDataChange?.(); }); ``` `host` 中常用字段: ```ts host.widgetId; // 当前组件实例 id host.dataConfig; // 当前组件注册的数据面板配置 host.props; // 私有属性值 host.publicProps; // 公共属性值 host.getQueryState(); // 读取插件端运行时查询状态 host.updateQueryState(patch); // 更新插件端运行时查询状态 host.on(name, handler); // 监听主应用推送给插件端的事件 host.off(name, handler); // 取消监听 host.once(name, handler); // 只监听一次 host.refreshData(); // 请求主应用刷新当前组件数据 host.emit(name, payload); // 向主应用发送插件事件 ``` 内置事件统一通过 `EventNames` 枚举使用,避免插件端手写字符串: ```txt EventNames.DataChange 主应用查询成功后推送数据 EventNames.DataPropsChange 主应用数据面板属性修改完成后推送最新配置 EventNames.PrivatePropsChange 主应用私有属性修改完成后推送最新属性 EventNames.ContextChange 主应用上下文更新后触发 ``` `host.on()` 不限制事件名。后续主应用通过通用事件通道推送新事件时,插件端只需要监听对应名称: ```ts host.on('custom-event', (payload) => { // 处理自定义事件 }); ``` 插件端不要直接调用业务数据接口,也不要处理主应用查询的加载态或错误态。加载态和错误态由主应用外壳统一展示,插件只消费成功后的数据推送。 查询结果结构: ```ts interface DataPanelQueryResult { columns: Array<{ alias: string; dataType: string; name: string; }>; executeTime?: number; pageNum?: number; pageSize?: number; queryId?: string; rows: Record[]; total: number; } ``` 示例: ```ts function handleDataChange(data?: DataPanelQueryResult) { const columns = data?.columns ?? []; const rows = data?.rows ?? []; tableInstance.value?.updateOption({ columns: columns.map((column) => ({ field: column.name, title: column.alias || column.name, })), records: rows, }); } let unsubscribeDataChange: (() => void) | undefined; onMounted(() => { unsubscribeDataChange = props.host?.on(EventNames.DataChange, handleDataChange); }); onBeforeUnmount(() => { unsubscribeDataChange?.(); }); ``` ## 设计数据面板 数据面板由 `dataConfig` 定义。每个 `area` 对应右侧数据页签里的一个字段投放区域。 ```ts const dataConfig: WidgetDataConfigDefinition = { enabled: true, areas: [ { key: 'tableColumns', label: '列', enabled: true, min: 1, max: -1, allowedFieldTypes: ['DIMENSION', 'MEASURE', 'CALC_COLUMN', 'CALC_MEASURE'], icon: '', }, ], }; ``` 字段说明: ```txt key 区域唯一标识,字段投放后会记录到 areaKey label 右侧数据面板展示名称 enabled 是否启用该区域 min 最少字段数 max 最多字段数,-1 表示不限制 allowedFieldTypes 允许拖入的字段类型 icon 区域标题前的图标,可选 ``` 当前主应用查询参数只需要字段的 `fieldId`。字段拖入、删除、排序后,主应用会先触发插件端监听的 `EventNames.DataPropsChange` 事件,再主动刷新数据,并在成功后触发插件端监听的 `EventNames.DataChange` 事件。 ## 数据面板属性修改事件 右侧数据面板中的字段拖入、删除、排序或模型变化后,主应用会推送 `EventNames.DataPropsChange`。这个事件只表示数据面板配置已经变化,不代表新数据已经查询完成;查询成功后的数据仍然通过 `EventNames.DataChange` 推送。 ```ts import type { DataPanelDataPropsChangePayload } from './types/render'; import { EventNames } from './types/render'; function handleDataPropsChange(change: DataPanelDataPropsChangePayload) { // change.dataProps 是最新数据面板属性 // change.previousDataProps 是修改前的数据面板属性快照 // 可以在这里清理字段映射缓存、重置列配置或重置分页到第一页 } let unsubscribeDataPropsChange: (() => void) | undefined; onMounted(() => { unsubscribeDataPropsChange = props.host?.on( EventNames.DataPropsChange, handleDataPropsChange, ); }); onBeforeUnmount(() => { unsubscribeDataPropsChange?.(); }); ``` 数据面板属性结构: ```ts interface DataPanelWidgetDataProps { fields: Array<{ areaKey: string; dataFormat?: number | string; fieldId: number | string; fieldName?: string; fieldType?: string; }>; modelId?: null | number | string; refreshKey?: number; version?: number; } ``` ## 构建查询参数 数据查询始终由主应用发起。插件不能调用 `queryDataModelData()`,也不能直接传完整 `queryParam`,但可以通过注册 `buildQueryParams(context)` 在主应用请求前补充分页、参数等查询扩展项。 主应用查询前会按这个顺序处理: ```txt 主应用按右侧数据面板构建基础查询参数 -> 执行插件注册的 buildQueryParams(context) -> 合并插件返回的分页、params 等扩展项 -> 主应用调用 queryDataModelData(queryParam) -> 查询成功后触发 EventNames.DataChange ``` `buildQueryParams` 示例: ```ts import type { DataPanelWidgetQueryBuildContext, DataPanelWidgetQueryParamsPatch, } from './types/context'; function buildQueryParams({ props, queryState, }: DataPanelWidgetQueryBuildContext): DataPanelWidgetQueryParamsPatch { const pagination = queryState.pagination ?? {}; const defaultPageSize = Number(props.pageSize ?? 20); return { enablePage: true, pageNum: pagination.pageNum ?? 1, pageSize: pagination.pageSize ?? defaultPageSize, totalRows: pagination.totalRows ?? true, }; } export function register(ctx: DataPanelPluginContext) { ctx.registerWidget({ buildQueryParams, dataConfig, renderer: createVueWidgetRenderer(Widget), }); } ``` 上面是分页表格的写法,首次查询也会使用默认分页。如果组件不需要分页或查询参数扩展,不注册 `buildQueryParams` 即可。 `context` 常用字段: ```txt widgetId 当前组件实例 id reason 本次刷新原因,例如 mount、plugin-request、data-props-change queryState 插件端运行时查询状态 props 当前私有属性 publicProps 当前公共属性 dataProps 当前数据面板中的模型和字段配置 dataConfig 插件注册的数据面板定义 ``` `buildQueryParams` 只返回查询扩展项。主应用会忽略插件返回值里的 `modelId`、`queryFields`、`fields`、`metadata`、`allowCrossSource` 等基础查询字段,这些字段只能由主应用根据右侧数据面板配置生成。 支持返回的常用字段: ```txt enablePage 是否启用分页 pageNum 当前页码,从 1 开始 pageSize 每页条数 totalRows 是否返回总数 params 查询参数数组 cache 是否启用缓存 ``` `params` 示例: ```ts return { params: [ { dataType: 'NUMBER', paramName: 'year', paramValue: 2026, }, ], }; ``` 如果分页控件在插件端,插件组件先更新运行时查询状态,再请求主应用刷新: ```ts async function handlePageChange(pageNum: number, pageSize: number) { props.host?.updateQueryState({ pagination: { pageNum, pageSize, totalRows: true, }, }); await props.host?.refreshData(); } ``` 插件端接收返回数据后更新自己的分页控件: ```ts const pageNum = ref(1); const pageSize = ref(20); const total = ref(0); function handleDataChange(data?: DataPanelQueryResult) { pageNum.value = data?.pageNum ?? pageNum.value; pageSize.value = data?.pageSize ?? pageSize.value; total.value = data?.total ?? 0; } ``` 运行时查询状态不等同于私有属性。`pageSizeOptions`、默认页大小等用户配置可以放在 `propertySchema` 和 `defaultProps` 中;当前页码、当前插件内筛选值这类临时状态应放在 `queryState` 中。 ## 添加私有属性配置 私有属性由两部分组成: - `defaultProps`:私有属性默认值,通常写在 `src/main.ts`。 - `propertySchema`:右侧属性面板配置,统一放在 `src/types/property-schema.ts`。 `src/main.ts` 只负责引入并注册: ```ts import { propertySchema } from './types/property-schema'; const defaultProps = { pageSize: 20, showIndex: true, }; export function register(ctx: DataPanelPluginContext) { ctx.registerWidget({ defaultProps, propertySchema, renderer: createVueWidgetRenderer(Widget), }); } ``` `src/types/property-schema.ts` 用来声明右侧属性面板展示哪些配置项: ```ts import type { WidgetPropertyGroup } from './context'; const propertySchema: WidgetPropertyGroup[] = [ { type: 'group', label: '表格设置', props: [ { type: 'field', key: 'pageSize', label: '显示条数', control: 'InputNumber', componentProps: { min: 1, max: 100, }, }, { type: 'field', key: 'showIndex', label: '显示序号', control: 'Switch', }, ], }, ]; export { propertySchema }; ``` ### 属性面板配置说明 `propertySchema` 是一个 `WidgetPropertyGroup[]`,主应用会根据它渲染右侧属性面板。最外层通常是多个分组,每个分组里放具体字段或子分组。 分组配置: ```txt type 分组类型,支持 group、Group、collspan、Collspan;不写时按 group 处理 label 分组标题,右侧属性面板折叠栏展示名称 key 可选,作为子字段的属性命名空间 props 子配置项,可以是字段,也可以继续嵌套分组 showLabel 可选,嵌套分组是否显示自身 label span 可选,分组内部按几列展示,默认 1 dependencies 分组显隐依赖,通过 dependencies.show(values) 控制是否展示 ``` 分组的 `dependencies.show(values)` 使用父级上下文计算。比如某个 `key: 'pager'` 分组内部的子分组依赖,`values` 会优先取 `host.props.pager`;顶层分组依赖则直接取完整 `host.props`。 字段配置: ```txt type 字段类型,通常写 field key 属性键名,对应 host.props 中的值 label 字段标题 control 控件类型 showLabel 是否展示字段标题,默认展示 componentProps 透传给控件的参数,如 min、max、step、options、placeholder dependencies 字段显隐依赖,目前通过 dependencies.show(values) 控制是否展示 ``` `componentProps.icon`、`componentProps.prefix`、`componentProps.suffix`、`componentProps.addonBefore`、`componentProps.addonAfter` 使用主应用注册的全局图标 key。插件端只传 `bi-icon-*` 字符串,不传 Vue 组件、VNode、`h()` 结果或返回 VNode 的函数。图标组件由主应用在右侧属性面板内渲染。 ```ts const propertyIconKeys = { alignLeft: 'bi-icon-align-left', } as const; export const propertySchema: WidgetPropertyGroup[] = [ { label: '分页', props: [ { key: 'align', control: 'Button', showLabel: false, componentProps: { icon: propertyIconKeys.alignLeft, }, props: { value: 'left', unValue: '', }, }, ], }, ]; ``` 当前主应用内置图标 key: ```txt bi-icon-align-left bi-icon-align-center bi-icon-align-right bi-icon-text-bold bi-icon-text-italic bi-icon-text-underline bi-icon-radius-top-left bi-icon-radius-top-right bi-icon-radius-bottom-left bi-icon-radius-bottom-right ``` `key` 的取值规则: ```ts const defaultProps = { pageSize: 20, style: { color: '#1677ff', opacity: 1, }, }; ``` ```ts export const propertySchema: WidgetPropertyGroup[] = [ { key: 'style', label: '样式', props: [ // 保存到 host.props.style.color { key: 'color', label: '颜色', control: 'ColorPicker', type: 'field' }, // key 中包含点路径时,按完整路径保存,不再拼接父级 group.key { key: 'style.opacity', label: '透明度', control: 'InputNumber', type: 'field' }, ], }, ]; ``` 控件类型: ```txt Input 文本输入,常用 componentProps.placeholder InputNumber 数字输入,常用 componentProps.min/max/step Switch 开关,值通常是 boolean Radio 单选,options 放在 componentProps.options Select 下拉选择,options 放在 componentProps.options ColorPicker 颜色选择,值通常是 #RRGGBB 字符串 UploadImage 图片上传,值是 base64 data URL ``` `Radio` 和 `Select` 的 `options` 示例: ```ts { key: 'align', label: '对齐', control: 'Select', type: 'field', componentProps: { options: [ { label: '居左', value: 'left' }, { label: '居中', value: 'center' }, { label: '居右', value: 'right' }, ], }, } ``` 字段显隐示例: ```ts { key: 'titleText', label: '标题内容', control: 'Input', type: 'field', dependencies: { show: (values) => values.showTitle === true, triggerFields: ['showTitle'], }, } ``` 右侧属性面板搜索会匹配分组标题、字段标题、字段 key、控件类型以及选项文案。 插件中读取私有属性: ```ts const pageSize = computed(() => Number(props.host?.props.pageSize ?? 20)); const showIndex = computed(() => Boolean(props.host?.props.showIndex)); ``` 嵌套属性可以用分组 `key` 或点路径表达,例如 `style.color`。 ## 私有属性修改后事件 私有属性的修改入口在主应用右侧属性面板。用户修改配置后,主应用会先更新组件实例的私有属性,再通过 `renderer.update()` 更新 `host.props`,最后触发插件端监听的 `EventNames.PrivatePropsChange` 事件。 插件端不需要 watch `host.props`,把私有属性修改后的逻辑集中写在事件监听里: ```ts import type { DataPanelPrivatePropsChangePayload } from './types/render'; import { EventNames } from './types/render'; function handlePrivatePropsChange(change: DataPanelPrivatePropsChangePayload) { // change.props 是最新私有属性 // change.previousProps 和 change.patch 仅在主应用通过补丁方式更新时存在 // 可以在这里重算样式、主题、列宽、图表配置或其他插件内部状态 } let unsubscribePrivatePropsChange: (() => void) | undefined; onMounted(() => { unsubscribePrivatePropsChange = props.host?.on( EventNames.PrivatePropsChange, handlePrivatePropsChange, ); }); onBeforeUnmount(() => { unsubscribePrivatePropsChange?.(); }); ``` 常见做法是把数据更新和私有属性更新汇总到同一个内部渲染函数: ```ts let latestData: DataPanelQueryResult | undefined; function updateView(data = latestData, privateProps = props.host?.props ?? {}) { // 根据最新数据和最新私有属性更新图表或表格 } function handleDataChange(data?: DataPanelQueryResult) { latestData = data; updateView(data); } function handlePrivatePropsChange(change: DataPanelPrivatePropsChangePayload) { updateView(latestData, change.props); } let unsubscribeDataChange: (() => void) | undefined; let unsubscribePrivatePropsChange: (() => void) | undefined; onMounted(() => { unsubscribeDataChange = props.host?.on(EventNames.DataChange, handleDataChange); unsubscribePrivatePropsChange = props.host?.on( EventNames.PrivatePropsChange, handlePrivatePropsChange, ); }); onBeforeUnmount(() => { unsubscribeDataChange?.(); unsubscribePrivatePropsChange?.(); }); ``` 如果插件内部交互需要通知主应用,不要直接修改 `host.props`,请使用 `host.emit()`: ```ts function handleRowClick(row: Record) { props.host?.emit('row-click', row); } ``` 主应用收到事件后决定是否更新属性、联动其他组件或执行其他动作。插件端不要直接修改主应用状态。 ## 刷新数据 插件可以提供刷新按钮,但它只能请求主应用刷新,不能自己查询业务数据。 ```ts async function handleRefresh() { await props.host?.refreshData(); } ``` 刷新链路: ```txt 插件调用 host.refreshData() -> 主应用按当前 dataConfig 和字段配置构造查询参数 -> 主应用执行 buildQueryParams(context) 合并插件端运行时查询状态 -> 主应用调用 queryDataModelData() -> 主应用显示加载或错误状态 -> 查询成功后调用 renderer.pushEvent(EventNames.DataChange, data) -> Vue 适配层触发 host.on(EventNames.DataChange) 监听函数 ``` `DataPanelWidgetHost` 中没有 `data` 属性。主应用推送给插件端的通知统一走 `renderer.pushEvent(eventName, payload)`,插件端统一用 `host.on(eventName, handler)` 监听。 ## 静态资源 插件自带静态资源放在: ```txt public/assets/ ``` 构建后输出到: ```txt dist/assets/ ``` 发布后可通过插件根路径访问: ```txt {plugin_base_url}assets/example.png ``` 需要在插件代码中拼接资源地址时,使用注册上下文的 `baseUrl`: ```ts const imageUrl = `${ctx.baseUrl}assets/example.png`; ``` ## 包体优化 远程插件入口 `dist/index.js` 会在主应用加载组件定义时请求。入口文件应尽量小,表格、图表、地图、编辑器等大依赖不要在入口静态引入,建议放到组件真正渲染时动态加载。 推荐写法: ```ts type VTableModule = typeof import('@visactor/vtable/es/ListTable-simple'); let vtableModulePromise: Promise | undefined; function loadVTableModule() { // 大型表格引擎按需加载,避免放大远程插件入口。 vtableModulePromise ??= import('@visactor/vtable/es/ListTable-simple'); return vtableModulePromise; } ``` 避免写法: ```ts import * as VTable from '@visactor/vtable'; ``` 这样会把 VTable 打进 `index.js`,主应用加载插件定义时就需要下载完整表格引擎。 当前模板会把 VTable 拆到 `dist/assets/vtable-*.js`。上传插件包时,`dist/assets/` 内的 chunk 必须和 `index.js` 一起发布,主应用会通过真实远程 URL 直接 `import(index.js)`,并由浏览器继续加载入口中引用的相对 chunk。 ## 打包 构建插件: ```sh pnpm run build ``` 生成可上传的插件包: ```sh pnpm run pack:plugin ``` 也可以使用 npm 的 pack 钩子: ```sh pnpm pack ``` 打包后产物: ```txt dist/ index.js style.css manifest.json assets/ dist.zip ``` `dist.zip` 根目录应包含 `index.js`、`manifest.json`,`style.css` 可选。只要构建产物里出现 `assets/*.js`,`assets/` 就必须一起上传,否则动态 chunk 无法加载。