@agions/taroviz 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agions/taroviz",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "基于 Taro 和 ECharts 的多端图表组件库",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",
@@ -76,9 +76,7 @@ class H5Adapter implements Adapter {
76
76
  // 获取容器元素
77
77
  const container = this.containerRef?.current || document.getElementById(this.canvasId);
78
78
  if (!container) {
79
- console.error('[TaroViz] H5Adapter: container not found');
80
- // 如果容器未找到,返回一个空对象
81
- return {} as EChartsType;
79
+ throw new Error(`[TaroViz] H5Adapter: container not found (canvasId: ${this.canvasId})`);
82
80
  }
83
81
 
84
82
  // 初始化图表
@@ -26,6 +26,7 @@ const PLATFORM_CONFIGS: Record<PlatformType, PlatformConfig> = {
26
26
  [PlatformType.DD]: { name: 'DingTalk', requireComponent: true },
27
27
  [PlatformType.QYWX]: { name: 'QiyeWechat' },
28
28
  [PlatformType.LARK]: { name: 'Lark' },
29
+ [PlatformType.KWAI]: { name: 'Kwai', requireComponent: true },
29
30
  [PlatformType.HARMONY]: { name: 'HarmonyOS', requireComponent: true },
30
31
  };
31
32
 
@@ -99,25 +100,40 @@ export function getEnv(): 'h5' | 'weapp' | 'unknown' {
99
100
 
100
101
  /**
101
102
  * 创建适配器实例
103
+ * 使用静态导入避免动态 require 的 ESM 问题
102
104
  */
103
105
  function createAdapterInstance(platform: PlatformType, options: AdapterOptions): Adapter {
104
- const adapters: Record<PlatformType, () => Adapter> = {
105
- [PlatformType.H5]: () => require('./h5').default.create(options),
106
- [PlatformType.WEAPP]: () => require('./weapp').default.create(options),
107
- [PlatformType.SWAN]: () => require('./swan').default.create(options),
108
- [PlatformType.TT]: () => require('./tt').default.create(options),
109
- [PlatformType.HARMONY]: () => require('./harmony').default.create(options),
110
- [PlatformType.ALIPAY]: () => require('./h5').default.create(options),
111
- [PlatformType.QQ]: () => require('./h5').default.create(options),
112
- [PlatformType.JD]: () => require('./h5').default.create(options),
113
- [PlatformType.DD]: () => require('./h5').default.create(options),
114
- [PlatformType.QYWX]: () => require('./h5').default.create(options),
115
- [PlatformType.LARK]: () => require('./h5').default.create(options),
116
- };
117
-
118
- return adapters[platform]();
106
+ // 预加载所有适配器(编译时解析,支持 tree-shaking)
107
+ switch (platform) {
108
+ case PlatformType.H5:
109
+ case PlatformType.ALIPAY:
110
+ case PlatformType.QQ:
111
+ case PlatformType.JD:
112
+ case PlatformType.DD:
113
+ case PlatformType.QYWX:
114
+ case PlatformType.LARK:
115
+ case PlatformType.KWAI:
116
+ return h5Adapter.create(options);
117
+ case PlatformType.WEAPP:
118
+ return weappAdapter.create(options);
119
+ case PlatformType.SWAN:
120
+ return swanAdapter.create(options);
121
+ case PlatformType.TT:
122
+ return ttAdapter.create(options);
123
+ case PlatformType.HARMONY:
124
+ return harmonyAdapter.create(options);
125
+ default:
126
+ return h5Adapter.create(options);
127
+ }
119
128
  }
120
129
 
130
+ // 静态导入适配器
131
+ import h5Adapter from './h5';
132
+ import weappAdapter from './weapp';
133
+ import swanAdapter from './swan';
134
+ import ttAdapter from './tt';
135
+ import harmonyAdapter from './harmony';
136
+
121
137
  /**
122
138
  * 获取适配器
123
139
  */
@@ -136,7 +152,7 @@ export function getAdapter(options: AdapterOptions): Adapter {
136
152
  return createAdapterInstance(platform, options);
137
153
  } catch (error) {
138
154
  console.error(`[TaroViz] Failed to load adapter for platform '${platform}':`, error);
139
- return require('./h5').default.create(options);
155
+ return h5Adapter.create(options);
140
156
  }
141
157
  }
142
158
 
@@ -158,9 +174,4 @@ export default {
158
174
  getAdapter,
159
175
  getEnv,
160
176
  detectPlatform,
161
- h5: require('./h5').default,
162
- weapp: require('./weapp').default,
163
- swan: require('./swan').default,
164
- tt: require('./tt').default,
165
- harmony: require('./harmony').default,
166
177
  };
@@ -8,7 +8,7 @@ import React, { useEffect, useRef, useMemo } from 'react';
8
8
  import { getAdapter } from '../../adapters';
9
9
  import { uuid } from '../../core/utils';
10
10
  import { BaseChartProps } from '../types';
11
- import { processAdapterConfig, safeRenderAdapter } from '../utils';
11
+ import { processAdapterConfig } from '../utils';
12
12
 
13
13
  /**
14
14
  * 基础图表包装组件
@@ -118,18 +118,12 @@ const BaseChartWrapper: React.FC<BaseChartProps & { chartType: string }> = ({
118
118
  ...style,
119
119
  };
120
120
 
121
- // 使用带有处理的适配器配置创建适配器实例
122
- const adapter = getAdapter(adapterConfig);
123
-
124
121
  return (
125
122
  <div
126
123
  className={`taroviz-${chartType} ${className}`}
127
124
  style={mergedStyle}
128
125
  ref={containerRef as React.RefObject<HTMLDivElement>}
129
- >
130
- {/* 安全地渲染适配器 */}
131
- {safeRenderAdapter(adapter)}
132
- </div>
126
+ />
133
127
  );
134
128
  };
135
129
 
@@ -11,6 +11,15 @@ export const CHART_INSTANCES: Record<string, EChartsType> = {};
11
11
  * @param instance 图表实例
12
12
  */
13
13
  export function registerChart(id: string, instance: EChartsType): void {
14
+ // 如果已存在同名ID,先释放旧实例防止内存泄漏
15
+ if (CHART_INSTANCES[id]) {
16
+ try {
17
+ console.warn(`[TaroViz] Chart instance '${id}' already exists, replacing and disposing old instance`);
18
+ CHART_INSTANCES[id].dispose();
19
+ } catch (e) {
20
+ console.warn(`Failed to dispose old chart instance: ${id}`, e);
21
+ }
22
+ }
14
23
  CHART_INSTANCES[id] = instance;
15
24
  }
16
25
 
@@ -79,6 +79,20 @@ export class CodeGenerator {
79
79
  });
80
80
  }
81
81
 
82
+ /**
83
+ * 转义代码生成用的字符串,防止 XSS
84
+ */
85
+ private escapeForCode(str: string): string {
86
+ if (!str) return '';
87
+ // 转义特殊字符防止代码注入
88
+ return str
89
+ .replace(/\\/g, '\\\\')
90
+ .replace(/`/g, '\\`')
91
+ .replace(/\$/g, '\\$')
92
+ .replace(/\{/g, '\\{')
93
+ .replace(/\}/g, '\\}');
94
+ }
95
+
82
96
  /**
83
97
  * 初始化内置模板
84
98
  */
@@ -511,15 +525,15 @@ export default chart;`,
511
525
  // 替换模板变量
512
526
  let code = template.content;
513
527
 
514
- // 替换组件名称
515
- const componentName = options.componentName || 'ChartComponent';
528
+ // 替换组件名称(转义防止XSS)
529
+ const componentName = this.escapeForCode(options.componentName || 'ChartComponent');
516
530
  code = code.replace(/\{componentName\}/g, componentName);
517
531
 
518
- // 替换图表ID
519
- const chartId = options.chartId || 'chart';
532
+ // 替换图表ID(转义防止XSS)
533
+ const chartId = this.escapeForCode(options.chartId || 'chart');
520
534
  code = code.replace(/\{chartId\}/g, chartId);
521
535
 
522
- // 替换选项
536
+ // 替换选项(JSON.stringify已处理转义,但模板变量要转义)
523
537
  const optionStr = JSON.stringify(option, null, 2);
524
538
  code = code.replace(/\{\s*option\s*\}/g, optionStr);
525
539
 
@@ -4,11 +4,15 @@
4
4
  * @returns 唯一标识符字符串
5
5
  */
6
6
  export function uuid(): string {
7
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
8
- const r = (Math.random() * 16) | 0;
9
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
10
- return v.toString(16);
11
- });
7
+ // 使用 crypto API 生成加密安全的 UUID
8
+ if (typeof globalThis.crypto?.randomUUID === 'function') {
9
+ return globalThis.crypto.randomUUID();
10
+ }
11
+
12
+ // 回退:使用 Math.random() + 时间戳混合
13
+ const timestamp = Date.now().toString(36);
14
+ const randomPart = Math.random().toString(36).substring(2, 15);
15
+ return `${timestamp}-${randomPart}-${Math.random().toString(36).substring(2, 10)}`;
12
16
  }
13
17
 
14
18
  /**
@@ -410,23 +410,35 @@ export function useDataPolling<T>(
410
410
  const [data, setData] = useState<T | null>(null);
411
411
  const [loading, setLoading] = useState(autoStart);
412
412
  const [error, setError] = useState<Error | null>(null);
413
- const [refreshIndex, setRefreshIndex] = useState(0);
413
+
414
+ // 用于取消进行中的请求
415
+ const abortRef = useRef<{ cancelled: boolean }>({ cancelled: false });
414
416
 
415
417
  const fetchData = useCallback(async () => {
418
+ // 取消之前的请求
419
+ abortRef.current.cancelled = true;
420
+ // 创建新的取消标记
421
+ abortRef.current = { cancelled: false };
422
+ const currentAbort = abortRef.current;
423
+
416
424
  let retries = retryCount;
417
425
  setLoading(true);
418
426
  setError(null);
419
427
 
420
- while (retries >= 0) {
428
+ while (retries >= 0 && !currentAbort.cancelled) {
421
429
  try {
422
430
  const result = await fetchFn();
423
- setData(result);
424
- setLoading(false);
431
+ if (!currentAbort.cancelled) {
432
+ setData(result);
433
+ setLoading(false);
434
+ }
425
435
  return;
426
436
  } catch (e) {
427
437
  retries--;
428
- if (retries < 0) {
429
- setError(e as Error);
438
+ if (retries < 0 || currentAbort.cancelled) {
439
+ if (!currentAbort.cancelled) {
440
+ setError(e as Error);
441
+ }
430
442
  setLoading(false);
431
443
  } else {
432
444
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
@@ -442,12 +454,18 @@ export function useDataPolling<T>(
442
454
 
443
455
  if (interval > 0) {
444
456
  const timer = setInterval(fetchData, interval);
445
- return () => clearInterval(timer);
457
+ return () => {
458
+ clearInterval(timer);
459
+ abortRef.current.cancelled = true;
460
+ };
446
461
  }
447
- }, [interval, autoStart, fetchData, refreshIndex]);
462
+
463
+ return () => {
464
+ abortRef.current.cancelled = true;
465
+ };
466
+ }, [interval, autoStart, fetchData]);
448
467
 
449
468
  const refresh = useCallback(() => {
450
- setRefreshIndex((prev) => prev + 1);
451
469
  fetchData();
452
470
  }, [fetchData]);
453
471