react-to-axure について
React コンポーネントを Axure にインポートできる形式へ変換するスキルです。プロトタイプ向けの宣言、ランタイムラッパー、ビルド出力の要件を扱います。
主な機能
- 01イベント、アクション、変数、設定、データの宣言
- 02forwardRef と useImperativeHandle によるラップ
- 03ビジュアル設定パネル項目の定義
- 04IIFE 出力と React 依存関係の外部化
- 05Axure 出力ファイルとリソースマニフェストの生成
活用シーン
- 01既存の React コンポーネントを Axure プロトタイプで利用
- 02コンポーネントの状態やイベントを Axure と連携
- 03コンポーネントの props 向け設定パネルを生成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ lintendo/ axhub-skills --skill react-to-axureインストール後、Claude Code で/react-to-axureを入力して読み込まれたか確認します。
同梱ファイルreferences/axure-types.tsreferences/config-panel-types.ts
詳細
React を Axure にエクスポート
概要
このスキルは、任意の React コンポーネントまたはアプリケーションを、Axure プロトタイププラットフォームにインポート可能な標準コンポーネントに変換するのに役立ちます。変換後、コンポーネントは以下の機能を備えます。
- 設定パネル:Axure エディターで視覚的に設定できる項目(色、テキスト、数値など)を提供します
- イベントシステム:Axure プロトタイプにカスタムイベントを送信します
- アクションシステム:Axure プロトタイプからの外部呼び出しに応答します
- 変数の公開:内部状態を Axure プロトタイプから読み取れるようにします
- データ接続:Axure プロトタイプから外部データソースを受け取ります
このスキルでは、次の 2 種類の成果物を生成します。
- 設定宣言:5 つの定数(EVENT_LIST / ACTION_LIST / VAR_LIST / CONFIG_LIST / DATA_LIST)
- ランタイムラッパー:
forwardRef+useImperativeHandleを使用したコンポーネントコード
また、ホストツールで利用する Axure エクスポート成果物は、デフォルトで共通の artifact ディレクトリに書き込まれます。
.axhub/make/artifacts/axure/<resource-id>/
├── index-bundle.json
├── axure-json.json
├── cover.svg
└── manifest.json
対象プロジェクトで .axhub/make/project.json を使用してリソースを記述する場合は、make-server やその他のホストが直接読み取り、コピー、ダウンロードできるように、対応する resources.prototypes[].artifacts.axure にこれらのパスを登録する必要があります。
使用する場面
- 既存の React コンポーネントを Axure プロトタイプで使用したい場合
- React アプリケーションを Axure プロトタイプシステムと双方向に連携させたい場合
- React コンポーネント用の標準化された設定パネル定義を生成したい場合
参照ファイル
このスキルには完全な型定義ファイルが付属しており、対象プロジェクトにそのままコピーして使用できます。
references/axure-types.ts:コアインターフェース(AxureProps、AxureHandle)とユーティリティ関数references/config-panel-types.ts:設定パネルコンポーネントの完全な型定義
.axhub/make/project.json のリソースメタデータの例:
{
"artifacts": {
"axure": {
"caseId": "home",
"manifestPath": ".axhub/make/artifacts/axure/home/manifest.json",
"indexBundlePath": ".axhub/make/artifacts/axure/home/index-bundle.json",
"axureJsonPath": ".axhub/make/artifacts/axure/home/axure-json.json",
"coverSvgPath": ".axhub/make/artifacts/axure/home/cover.svg"
}
}
}
変換ワークフロー
ステップ 1:ソースコンポーネントを分析する
対象の React コンポーネントについて、以下の点を分析します。
- Props インターフェース:コンポーネントが現在受け取る props は何ですか?
- 内部状態:公開が必要な useState / useReducer の状態はありますか?
- インタラクション:ユーザー操作のイベント(click、change、submit など)は何ですか?
- 外部依存関係:コンポーネントはどのサードパーティーライブラリに依存していますか?
- DOM 操作:DOM を直接操作しますか(グラフライブラリ、地図ライブラリなど)?
変換チェックリストを作成し、マッピングが必要な props → config、state → var、callback → event/action を列挙します。
ステップ 2:型定義をコピーする
references/axure-types.ts を対象プロジェクトの共通ディレクトリ(例:src/common/axure-types.ts)にコピーします。設定パネルの詳細な型が必要な場合は、references/config-panel-types.ts もコピーします。
ステップ 3:インターフェース層を定義する
分析結果に基づいて、次の 5 つの定数定義を生成します。
3.1 イベントリスト EVENT_LIST
コンポーネントが外部に発火できるイベントを定義します。
import type { EventItem } from './path-to/axure-types';
const EVENT_LIST: EventItem[] = [
{ name: 'onClick', desc: '点击时触发' },
{ name: 'onChange', desc: '值改变时触发,payload 为新值的 JSON 字符串' }
];
ルール:
name:イベント名。camelCase の使用を推奨しますdesc:イベントの説明。発火するタイミングを記述しますpayload:省略可能。payload の形式を記述します(payload は必ず文字列型にしてください。複雑なデータにはJSON.stringifyが必要です)
3.2 アクションリスト ACTION_LIST
コンポーネントが応答できる外部呼び出しを定義します。
import type { Action } from './path-to/axure-types';
const ACTION_LIST: Action[] = [
{ name: 'reset', desc: '重置组件到初始状态' },
{ name: 'setValue', desc: '设置值,参数格式:JSON 字符串', params: 'JSON string' }
];
ルール:
paramsは必ず文字列型にしてください。複雑なパラメーターはJSON.parseで解析する必要がありますdescにはパラメーターの形式を記述します
3.3 変数リスト VAR_LIST
コンポーネントが公開する内部状態を定義します。
import type { KeyDesc } from './path-to/axure-types';
const VAR_LIST: KeyDesc[] = [
{ name: 'current_value', desc: '当前值' },
{ name: 'is_loading', desc: '是否加载中' }
];
ルール:
nameは必ず snake_case(小文字 + 数字 + アンダースコア)にしてください- 外部からの読み取りが必要な状態のみを公開し、内部実装の詳細は公開しないでください
3.4 設定項目リスト CONFIG_LIST
設定パネルで設定可能な項目を定義します。
import type { ConfigItem } from './path-to/axure-types';
const CONFIG_LIST: ConfigItem[] = [
{
type: 'input',
attributeId: 'title',
displayName: '标题',
info: '组件顶部显示的标题文本',
initialValue: '默认标题'
},
{
type: 'colorPicker',
attributeId: 'primaryColor',
displayName: '主色调',
info: '组件的主题颜色',
initialValue: '#1890ff'
},
{
type: 'inputNumber',
attributeId: 'maxCount',
displayName: '最大数量',
info: '允许的最大数量',
initialValue: 10,
min: 1,
max: 100
},
{
type: 'switch',
attributeId: 'showBorder',
displayName: '显示边框',
info: '是否显示组件边框',
initialValue: true
},
{
type: 'select',
attributeId: 'size',
displayName: '尺寸',
info: '组件尺寸',
initialValue: 'medium',
options: [
{ label: '小', value: 'small' },
{ label: '中', value: 'medium' },
{ label: '大', value: 'large' }
]
}
];
使用可能な type(詳細な定義は references/config-panel-types.ts を参照):
| type | 用途 | initialValue の型 |
|---|---|---|
input | テキスト入力 | string |
inputNumber | 数値入力(min/max/step をサポート) | number |
checkbox | チェックボックス | boolean |
switch | スイッチ | boolean |
slider | スライダー(min/max が必要) | number |
select | ドロップダウン選択(options が必要) | string | number |
colorPicker | カラーピッカー | string(HEX 形式) |
autoComplete | オートコンプリート入力 | string |
arrayData | 配列データエディター | string[] |
table | テーブルデータエディター | any[] |
map | キーと値のペアのエディター | Record<string, any> |
fontSetting | フォント設定(色/サイズ/太さなど) | オブジェクト |
lineSetting | 線の設定 | オブジェクト |
group | グループコンテナー(children を含む) | — |
collapse | 折りたたみパネルコンテナー | — |
マッピング方針(元の Props → ConfigItem):
| 元の Props の型 | 推奨する ConfigItem type |
|---|---|
string | input |
number | inputNumber |
boolean | switch または checkbox |
enum / union | select(列挙値を options にマッピング) |
| 色文字列 | colorPicker |
string[] | arrayData |
| 複雑なオブジェクト/配列 | table または map |
3.5 データ項目リスト DATA_LIST
コンポーネントが受け取る外部データ構造を定義します。
import type { DataDesc } from './path-to/axure-types';
const DATA_LIST: DataDesc[] = [
{
name: 'items',
desc: '列表数据',
keys: [
{ name: 'id', desc: '唯一标识' },
{ name: 'title', desc: '标题' },
{ name: 'value', desc: '值' }
]
}
];
適用場面:リストやテーブルなど、データ駆動型コンポーネントに適しています。純粋な表示用または設定駆動のコンポーネントでは、空配列 [] を設定できます。
ステップ 4:コンポーネントをラップする
元のコンポーネントを forwardRef + useImperativeHandle でラップします。
import React, { forwardRef, useImperativeHandle, useState, useCallback } from 'react';
import type { AxureProps, AxureHandle } from './path-to/axure-types';
const WrappedComponent = forwardRef<AxureHandle, AxureProps>(
function WrappedComponent(innerProps, ref) {
// ① 安全解构 props(防御性编程)
const dataSource = innerProps && innerProps.data ? innerProps.data : {};
const configSource = innerProps && innerProps.config ? innerProps.config : {};
const onEventHandler = typeof innerProps.onEvent === 'function'
? innerProps.onEvent
: function () { return undefined; };
const container = innerProps && innerProps.container ? innerProps.container : null;
// ② 从 config 读取配置值(使用类型检查,避免 || 运算符)
const title = typeof configSource.title === 'string' && configSource.title
? configSource.title
: '默认标题';
const maxCount = typeof configSource.maxCount === 'number'
? configSource.maxCount
: 10;
// ③ 组件内部状态
const countState = useState(0);
const count = countState[0];
const setCount = countState[1];
// ④ 事件触发(payload 必须是字符串)
const emitEvent = useCallback(function (eventName: string, payload?: string) {
try {
onEventHandler(eventName, payload);
} catch (error) {
console.warn('事件触发失败:', eventName, error);
}
}, [onEventHandler]);
// ⑤ 动作处理(params 必须是字符串)
const handleAction = useCallback(function (name: string, params?: string) {
switch (name) {
case 'reset':
setCount(0);
break;
case 'setValue':
if (params) {
try {
const parsed = JSON.parse(params);
if (typeof parsed.count === 'number') {
setCount(parsed.count);
}
} catch (e) {
console.warn('参数解析失败:', e);
}
}
break;
}
}, []);
// ⑥ 暴露 Handle 接口
useImperativeHandle(ref, function () {
return {
getVar: function (name: string) {
const vars: Record<string, any> = {
current_value: count,
};
return vars[name];
},
fireAction: handleAction,
eventList: EVENT_LIST,
actionList: ACTION_LIST,
varList: VAR_LIST,
configList: CONFIG_LIST,
dataList: DATA_LIST
};
}, [count, handleAction]);
// ⑦ 渲染原始组件(透传改造后的 props)
return <OriginalComponent title={title} maxCount={maxCount} />;
}
);
export default WrappedComponent;
ステップ 5:ビルド出力を調整する
ビルド成果物は、以下の要件を満たす必要があります。
- 出力形式:IIFE(即時実行関数)。グローバル変数名は
UserComponent - 依存関係の外部化:React と ReactDOM は必ず外部化し、成果物にバンドルしないでください
- 単一ファイル出力:最終成果物は単一の JS ファイル + 任意の CSS ファイルとします
Vite のビルド設定例:
// vite.config.js
export default {
build: {
lib: {
entry: 'src/index.tsx',
name: 'UserComponent',
formats: ['iife'],
fileName: () => 'index.js'
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
}
}
};
ステップ 6:検証チェックリスト
変換が完了したら、各項目を確認します。
- コンポーネントが
forwardRef<AxureHandle, AxureProps>でラップされている -
useImperativeHandleですべての 5 つのリスト(eventList, actionList, varList, configList, dataList)が正しく公開されている -
export defaultでラップ済みコンポーネントがエクスポートされている - すべての event payload が文字列型である
- すべての action params が文字列型である
- VAR_LIST 内の name が snake_case を使用している
- CONFIG_LIST の各項目に
type、attributeId、displayName、initialValueがある - props の分割代入に防御的プログラミング(
innerProps && innerProps.xxx)を使用し、ES6 の分割代入を使用していない - config 値の読み取りに型チェックを使用し、
||演算子を使用していない(0/falseが誤判定されるのを防ぐため) - ビルド成果物の形式が IIFE で、グローバル変数名が
UserComponentである - React/ReactDOM が外部化され、重複してバンドルされていない
変換例
例 1:シンプルな表示コンポーネント
変換前(元の React コンポーネント):
function StatusCard({ title, status, color }) {
return (
<div style={{ borderLeft: `4px solid ${color}` }}>
<h3>{title}</h3>
<span>{status}</span>
</div>
);
}
変換後(Axure 互換コンポーネント):
import React, { forwardRef, useImperativeHandle } from 'react';
import type { AxureProps, AxureHandle, EventItem, Action, KeyDesc, ConfigItem, DataDesc } from './path-to/axure-types';
const EVENT_LIST: EventItem[] = [];
const ACTION_LIST: Action[] = [];
const VAR_LIST: KeyDesc[] = [
{ name: 'current_status', desc: '当前状态文本' }
];
const CONFIG_LIST: ConfigItem[] = [
{ type: 'input', attributeId: 'title', displayName: '标题', initialValue: '状态卡片' },
{ type: 'input', attributeId: 'status', displayName: '状态', initialValue: '正常' },
{ type: 'colorPicker', attributeId: 'color', displayName: '状态颜色', initialValue: '#52c41a' }
];
const DATA_LIST: DataDesc[] = [];
const Component = forwardRef<AxureHandle, AxureProps>(function StatusCard(innerProps, ref) {
const configSource = innerProps && innerProps.config ? innerProps.config : {};
const title = typeof configSource.title === 'string' && configSource.title ? configSource.title : '状态卡片';
const status = typeof configSource.status === 'string' && configSource.status ? configSource.status : '正常';
const color = typeof configSource.color === 'string' && configSource.color ? configSource.color : '#52c41a';
useImperativeHandle(ref, function () {
return {
getVar: function (name: string) { return name === 'current_status' ? status : undefined; },
fireAction: function () {},
eventList: EVENT_LIST,
actionList: ACTION_LIST,
varList: VAR_LIST,
configList: CONFIG_LIST,
dataList: DATA_LIST
};
}, [status]);
return (
<div style={{ borderLeft: '4px solid ' + color, padding: '12px' }}>
<h3>{title}</h3>
<span>{status}</span>
</div>
);
});
export default Component;
示例 2:交互型组件(带事件和动作)
改造前:
function Counter({ initial = 0, onCountChange }) {
const [count, setCount] = useState(initial);
return (
<div>
<span>{count}</span>
<button onClick={() => { setCount(c => c + 1); onCountChange?.(count + 1); }}>+1</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
);
}
改造后:
import React, { forwardRef, useImperativeHandle, useState, useCallback } from 'react';
import type { AxureProps, AxureHandle, EventItem, Action, KeyDesc, ConfigItem, DataDesc } from './path-to/axure-types';
const EVENT_LIST: EventItem[] = [{ name: 'onCountChange', desc: '计数改变时触发' }];
const ACTION_LIST: Action[] = [
{ name: 'increment', desc: '计数 +1' },
{ name: 'reset', desc: '重置计数' },
{ name: 'setCount', desc: '设置计数值,参数:数字字符串', params: 'number string' }
];
const VAR_LIST: KeyDesc[] = [{ name: 'count', desc: '当前计数值' }];
const CONFIG_LIST: ConfigItem[] = [
{ type: 'inputNumber', attributeId: 'initial', displayName: '初始值', initialValue: 0, min: 0 }
];
const DATA_LIST: DataDesc[] = [];
const Component = forwardRef<AxureHandle, AxureProps>(function Counter(innerProps, ref) {
const configSource = innerProps && innerProps.config ? innerProps.config : {};
const onEventHandler = typeof innerProps.onEvent === 'function'
? innerProps.onEvent : function () {};
const initial = typeof configSource.initial === 'number' ? configSource.initial : 0;
const countState = useState(initial);
const count = countState[0];
const setCount = countState[1];
const emitEvent = useCallback(function (eventName: string, payload?: string) {
try { onEventHandler(eventName, payload); } catch (e) { console.warn(e); }
}, [onEventHandler]);
const increment = useCallback(function () {
setCount(function (prev) {
var next = prev + 1;
emitEvent('onCountChange', String(next));
return next;
});
}, [emitEvent]);
const reset = useCallback(function () {
setCount(initial);
emitEvent('onCountChange', String(initial));
}, [initial, emitEvent]);
const handleAction = useCallback(function (name: string, params?: string) {
switch (name) {
case 'increment': increment(); break;
case 'reset': reset(); break;
case 'setCount':
if (params) {
var n = parseInt(params, 10);
if (!isNaN(n)) { setCount(n); emitEvent('onCountChange', String(n)); }
}
break;
}
}, [increment, reset, emitEvent]);
useImperativeHandle(ref, function () {
return {
getVar: function (name: string) { return name === 'count' ? count : undefined; },
fireAction: handleAction,
eventList: EVENT_LIST,
actionList: ACTION_LIST,
varList: VAR_LIST,
configList: CONFIG_LIST,
dataList: DATA_LIST
};
}, [count, handleAction]);
return (
<div>
<span>{count}</span>
<button onClick={increment}>+1</button>
<button onClick={reset}>重置</button>
</div>
);
});
export default Component;
示例 3:数据驱动型组件
改造前:
function UserList({ users, onSelect }) {
return (
<ul>
{users.map(u => (
<li key={u.id} onClick={() => onSelect(u)}>
{u.name} - {u.email}
</li>
))}
</ul>
);
}
改造后:
import React, { forwardRef, useImperativeHandle, useState, useCallback } from 'react';
import type { AxureProps, AxureHandle, EventItem, Action, KeyDesc, ConfigItem, DataDesc } from './path-to/axure-types';
const EVENT_LIST: EventItem[] = [{ name: 'onSelect', desc: '选中用户时触发,payload 为用户 JSON' }];
const ACTION_LIST: Action[] = [{ name: 'clearSelection', desc: '清除选中' }];
const VAR_LIST: KeyDesc[] = [{ name: 'selected_user_id', desc: '当前选中的用户 ID' }];
const CONFIG_LIST: ConfigItem[] = [
{ type: 'input', attributeId: 'emptyText', displayName: '空状态文本', initialValue: '暂无数据' }
];
const DATA_LIST: DataDesc[] = [
{
name: 'users',
desc: '用户列表数据',
keys: [
{ name: 'id', desc: '用户 ID' },
{ name: 'name', desc: '用户姓名' },
{ name: 'email', desc: '用户邮箱' }
]
}
];
const Component = forwardRef<AxureHandle, AxureProps>(function UserList(innerProps, ref) {
const dataSource = innerProps && innerProps.data ? innerProps.data : {};
const configSource = innerProps && innerProps.config ? innerProps.config : {};
const onEventHandler = typeof innerProps.onEvent === 'function'
? innerProps.onEvent : function () {};
const emptyText = typeof configSource.emptyText === 'string' && configSource.emptyText
? configSource.emptyText : '暂无数据';
var users = Array.isArray(dataSource.users) ? dataSource.users : [];
const selectedState = useState<string | null>(null);
const selectedId = selectedState[0];
const setSelectedId = selectedState[1];
const emitEvent = useCallback(function (eventName: string, payload?: string) {
try { onEventHandler(eventName, payload); } catch (e) { console.warn(e); }
}, [onEventHandler]);
const handleAction = useCallback(function (name: string) {
if (name === 'clearSelection') setSelectedId(null);
}, []);
useImperativeHandle(ref, function () {
return {
getVar: function (name: string) { return name === 'selected_user_id' ? selectedId : undefined; },
fireAction: handleAction,
eventList: EVENT_LIST,
actionList: ACTION_LIST,
varList: VAR_LIST,
configList: CONFIG_LIST,
dataList: DATA_LIST
};
}, [selectedId, handleAction]);
if (users.length === 0) return <div>{emptyText}</div>;
return (
<ul>
{users.map(function (u: any) {
return (
<li key={u.id} onClick={function () {
setSelectedId(u.id);
emitEvent('onSelect', JSON.stringify(u));
}}>
{u.name} - {u.email}
</li>
);
})}
</ul>
);
});
export default Component;
常见问题
Q: 组件使用了图表库(ECharts、D3)等直接操作 DOM 的库怎么办?
使用 innerProps.container 获取 DOM 容器,在 useEffect 中初始化图表引擎:
const container = innerProps && innerProps.container ? innerProps.container : null;
useEffect(function () {
if (!container) return;
const chart = echarts.init(container);
chart.setOption({ /* ... */ });
return function () { chart.dispose(); };
}, [container]);
return null; // 直接使用 container 渲染时返回 null
Q: 组件的 props 很复杂(嵌套对象),如何映射到 CONFIG_LIST?
- 扁平化:将嵌套属性用
attributeId的点分隔路径表示,如'style.fontSize' - 分组:使用
type: 'group'或type: 'collapse'组织相关配置项 - 复杂结构:使用
table或map类型编辑器
Q: 原始组件用了 props 解构、箭头函数,需要改写吗?
建议改写以提升兼容性:
- 使用
function关键字替代箭头函数 - 使用
innerProps && innerProps.xxx替代解构(防御性编程) - 使用
useState返回值的数组索引访问而非解构
Q: 如何处理组件的样式?
- 将 CSS 放在同目录的
style.css中并import './style.css' - 使用带组件前缀的类名避免全局冲突(如
my-chart-container) - 避免全局样式污染
类型参考
完整的类型定义见本技能的 references/ 目录:
-
references/axure-types.ts:核心接口和工具函数AxureProps:组件 Props 接口(data、config、onEvent、container)AxureHandle:组件 Handle 接口(getVar、fireAction、5 个列表)EventItem:事件定义{ name, desc, payload? }Action:动作定义{ name, desc, params? }KeyDesc:变量/数据字段定义{ name, desc }(name 必须 snake_case)DataDesc:数据源定义{ name, desc, keys: KeyDesc[] }ConfigItem:配置面板项定义safeEmitEvent、createEventEmitter:事件触发工具函数getConfigValue、getDataValue:安全取值工具函数
-
references/config-panel-types.ts:配置面板组件详细类型InputConfig、InputNumberConfig、CheckboxConfig、SliderConfigSelectConfig、AutoCompleteConfig、ColorPickerConfigArrayDataConfig、TableConfig、MapConfigFontSettingConfig、LineSettingConfig、PointSettingConfigGroupConfig:分组布局