skillby hunmer
Convert React To Memento
将 React 应用快速转换为适用于 Memento 加载的 WebView 应用。支持 Vite、Webpack 等构建工具,自动配置 Memento Mock 环境、类型定义和元数据。
Installs: 0
Used in: 1 repos
Updated: 2d ago
$
npx ai-builder add skill hunmer/convert-react-to-mementoInstalls to .claude/skills/convert-react-to-memento/
# React to Memento WebView 转换 Skill
## 工作流程
1. **分析现有项目**
- 检测项目类型(Vite/Webpack/CRA)
- 识别 index.html 入口文件
- 检查是否已有 Memento 配置
- 分析 package.json 依赖
- 检查 Tailwind CSS 使用方式(CDD 还是 npm)
- 检查字体配置方式
2. **询问用户需求**
- 应用名称和描述
- 需要哪些 Memento API 功能?
- 是否需要权限申请?
- 是否需要插件集成?
- 是否使用 Tailwind CSS?
- 需要使用哪些字体?
3. **配置 Memento Mock**
- 安装 `memento-mock` npm 包
- 在 index.tsx 中动态加载 Mock(开发环境)
4. **配置 Tailwind CSS(可选)**
- 从 CDN 迁移到 npm 包
- 配置 Tailwind CSS v4
- 创建 PostCSS 配置
5. **配置本地字体(可选)**
- 安装 @fontsource 字体包
- 移除 Google Fonts 外部链接
- 配置 CSS 导入本地字体
6. **创建/更新 metadata.json**
配置应用元数据和权限
7. **添加类型定义**
创建 memento.d.ts TypeScript 定义文件
8. **提供集成代码模板**
根据用户需求提供相应的代码示例
## Memento API 快速参考
### 系统 API
```typescript
// 获取当前时间
const time = await Memento.system.getCurrentTime();
// 获取设备信息
const device = await Memento.system.getDeviceInfo();
// 获取应用信息
const appInfo = await Memento.system.getAppInfo();
// 格式化日期
const formatted = await Memento.system.formatDate({
date: '2024-01-01',
format: 'YYYY-MM-DD HH:mm:ss'
});
// 获取时间戳
const timestamp = await Memento.system.getTimestamp();
// 获取自定义日期(相对天数)
const customDate = await Memento.system.getCustomDate({ days: 7 });
```
### UI API
```typescript
// 显示 Toast 消息
await Memento.ui.toast('操作成功!', { duration: 3000 });
// 显示 Alert 警告
await Memento.ui.alert('确认删除吗?');
// 显示对话框
const result = await Memento.ui.dialog({
title: '确认',
message: '是否继续?',
showCancel: true
});
// result.confirmed: boolean
```
### 存储 API
```typescript
// 写入数据(支持任意类型,JSON 序列化)
await Memento.storage.write('user', { name: '张三', age: 30 });
// 读取数据
const user = await Memento.storage.read('user');
// 删除数据
await Memento.storage.delete('user');
// 清空所有存储
await Memento.storage.clear();
// 获取所有键
const keys = await Memento.storage.keys();
```
### 插件 API
```typescript
// 日记插件
await Memento.plugins.diary.createEntry({
title: '新日记',
content: '日记内容...',
tags: ['JavaScript', 'Memento']
});
// 笔记插件
await Memento.plugins.notes.createNote({
title: '新笔记',
content: '笔记内容...'
});
// 自定义插件
await Memento.plugins.myPlugin.myMethod({
param1: 'value1',
param2: 'value2'
});
```
### 工具函数
```typescript
// 获取存储状态
const state = Memento.utils.getStorageState();
// 重置存储
await Memento.utils.resetStorage();
// 日志记录
Memento.utils.log('调试信息');
Memento.utils.error('错误信息');
Memento.utils.warn('警告信息');
```
## 项目配置步骤
### 1. 配置 Memento Mock
**安装依赖:**
```bash
pnpm add -D memento-mock
```
**在 index.tsx 中动态加载 Mock(推荐):**
```tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
// 仅在开发环境加载 Memento Mock
if (import.meta.env.DEV && typeof window.Memento === 'undefined') {
import('memento-mock').then((module) => {
module.default(); // 手动初始化
console.log('Memento Mock loaded');
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
});
} else {
// 生产环境或 Memento 已存在
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}
```
**配置环境变量(.env.local):**
```env
VITE_GEMINI_API_KEY=your_api_key_here
```
### 2. 配置 Tailwind CSS v4(从 CDN 迁移到 npm)
**安装依赖:**
```bash
pnpm add -D tailwindcss @tailwindcss/postcss postcss autoprefixer
```
**创建 postcss.config.js:**
```javascript
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
```
**创建 index.css(使用 Tailwind v4 语法):**
```css
@import "tailwindcss";
@theme {
--font-family-roboto: 'Roboto', sans-serif;
--color-surface: #fdfcff;
}
/* 自定义样式 */
body {
font-family: var(--font-family-roboto);
background-color: var(--color-surface);
}
```
**在 index.tsx 中引入 CSS:**
```tsx
import './index.css';
```
**从 index.html 移除 CDN 引用:**
```html
<!-- 移除这一行 -->
<script src="https://cdn.tailwindcss.com"></script>
```
### 3. 配置本地字体(使用 @fontsource)
**安装字体包:**
```bash
pnpm add @fontsource/roboto
```
**在 index.css 中导入字体:**
```css
@import "tailwindcss";
/* 导入本地 Roboto 字体 */
@import "@fontsource/roboto/400.css";
@import "@fontsource/roboto/500.css";
@import "@fontsource/roboto/700.css";
@theme {
--font-family-roboto: 'Roboto', sans-serif;
}
```
**从 index.html 移除 Google Fonts 引用:**
```html
<!-- 移除这一行 -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
```
**常用字体包:**
- `@fontsource/roboto` - Roboto 字体
- `@fontsource/inter` - Inter 字体
- `@fontsource/noto-sans-sc` - 思源黑体(中文)
- `@fontsource/material-icons` - Material Icons
### 4. 更新 index.html(Vite 项目)
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<title>应用名称</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>
```
**注意:** Memento Mock 现在通过 index.tsx 动态加载,不需要在 index.html 中引用。
### 5. 创建 metadata.json
```json
{
"name": "应用名称",
"description": "应用描述",
"requestFramePermissions": []
}
```
### 6. 创建 memento.d.ts
```typescript
declare global {
interface Window {
Memento: {
ready: (callback: () => void) => void;
_ready: boolean;
system: {
getCurrentTime: () => Promise<string>;
getDeviceInfo: () => Promise<Record<string, any>>;
getAppInfo: () => Promise<Record<string, any>>;
formatDate: (params: { date: string; format: string }) => Promise<string>;
getTimestamp: () => Promise<number>;
getCustomDate: (params: { days: number }) => Promise<string>;
};
ui: {
toast: (message: string, options?: { duration?: number }) => Promise<void>;
alert: (message: string) => Promise<void>;
dialog: (options: {
title: string;
message: string;
showCancel?: boolean;
}) => Promise<{ confirmed: boolean }>;
};
storage: {
write: (key: string, value: any) => Promise<void>;
read: (key: string) => Promise<any>;
delete: (key: string) => Promise<void>;
clear: () => Promise<void>;
keys: () => Promise<string[]>;
};
plugins: Record<string, Record<string, (...args: any[]) => Promise<any>>>;
utils: {
getStorageState: () => Record<string, any>;
resetStorage: () => Promise<void>;
log: (message: string) => void;
error: (message: string) => void;
warn: (message: string) => void;
};
};
}
}
export {};
```
## 代码模板
### 模板 1: React 组件中使用 Memento
```tsx
import { useEffect, useState } from 'react';
function App() {
const [deviceInfo, setDeviceInfo] = useState<any>(null);
const [currentTime, setCurrentTime] = useState<string>('');
useEffect(() => {
// 等待 Memento 准备就绪
Memento.ready(async () => {
// 获取设备信息
const device = await Memento.system.getDeviceInfo();
setDeviceInfo(device);
// 获取当前时间
const time = await Memento.system.getCurrentTime();
setCurrentTime(time);
// 显示欢迎消息
await Memento.ui.toast('欢迎使用 Memento!');
});
}, []);
const handleSave = async () => {
// 保存数据
await Memento.storage.write('userSettings', {
theme: 'dark',
language: 'zh-CN'
});
await Memento.ui.toast('设置已保存!');
};
const handleLoad = async () => {
// 读取数据
const settings = await Memento.storage.read('userSettings');
console.log('用户设置:', settings);
};
return (
<div className="p-4">
<h1>Memento React 应用</h1>
{deviceInfo && (
<div>
<p>设备: {deviceInfo.model}</p>
<p>系统: {deviceInfo.platform}</p>
</div>
)}
<p>当前时间: {currentTime}</p>
<button onClick={handleSave}>保存设置</button>
<button onClick={handleLoad}>加载设置</button>
</div>
);
}
export default App;
```
### 模板 2: 使用自定义 Hook
```tsx
// hooks/useMemento.ts
import { useEffect, useState } from 'react';
export function useMemento() {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
Memento.ready(() => {
setIsReady(true);
});
}, []);
return { isReady };
}
export function useMementoStorage<T>(key: string, initialValue: T) {
const [data, setData] = useState<T>(initialValue);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
Memento.ready(async () => {
const stored = await Memento.storage.read(key);
if (stored !== undefined) {
setData(stored);
}
setIsLoading(false);
});
}, [key]);
const setStoredData = async (newValue: T) => {
setData(newValue);
await Memento.storage.write(key, newValue);
};
return { data, setData: setStoredData, isLoading };
}
```
使用 Hook:
```tsx
import { useMemento, useMementoStorage } from './hooks/useMemento';
function Settings() {
const { isReady } = useMemento();
const { data: settings, setData: setSettings } = useMementoStorage('settings', {
theme: 'light'
});
if (!isReady) return <div>加载中...</div>;
return (
<div>
<button onClick={() => setSettings({ theme: 'dark' })}>
切换到深色模式
</button>
<p>当前主题: {settings.theme}</p>
</div>
);
}
```
### 模板 3: 调用日记插件
```tsx
import { useState } from 'react';
function DiaryEntry() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const handleSubmit = async () => {
try {
await Memento.plugins.diary.createEntry({
title,
content,
tags: ['React', 'WebView']
});
await Memento.ui.toast('日记创建成功!');
setTitle('');
setContent('');
} catch (error) {
await Memento.ui.alert('创建失败: ' + error);
}
};
return (
<div className="p-4">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="标题"
className="w-full p-2 border rounded"
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="内容"
className="w-full p-2 border rounded mt-2"
rows={5}
/>
<button
onClick={handleSubmit}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
>
保存日记
</button>
</div>
);
}
```
### 模板 4: 对话框确认
```tsx
function DeleteButton({ onDelete }: { onDelete: () => void }) {
const handleDelete = async () => {
const result = await Memento.ui.dialog({
title: '确认删除',
message: '删除后无法恢复,是否继续?',
showCancel: true
});
if (result.confirmed) {
onDelete();
await Memento.ui.toast('已删除');
}
};
return (
<button onClick={handleDelete} className="text-red-500">
删除
</button>
);
}
```
### 模板 5: 完整的应用结构
```tsx
import { useEffect, useState } from 'react';
import { useMementoStorage } from './hooks/useMemento';
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
function TodoApp() {
const { isReady } = useMemento();
const { data: todos, setData: setTodos } = useMementoStorage<TodoItem[]>('todos', []);
const [inputValue, setInputValue] = useState('');
const addTodo = async () => {
if (!inputValue.trim()) return;
const newTodos = [...todos, {
id: Date.now(),
text: inputValue,
completed: false
}];
setTodos(newTodos);
setInputValue();
await Memento.ui.toast('已添加');
};
const toggleTodo = async (id: number) => {
const newTodos = todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
setTodos(newTodos);
};
const deleteTodo = async (id: number) => {
const result = await Memento.ui.dialog({
title: '确认',
message: '删除此待办?',
showCancel: true
});
if (result.confirmed) {
const newTodos = todos.filter(todo => todo.id !== id);
setTodos(newTodos);
await Memento.ui.toast('已删除');
}
};
if (!isReady) return <div className="p-4">正在初始化...</div>;
return (
<div className="p-4 max-w-md mx-auto">
<h1 className="text-2xl font-bold mb-4">待办事项</h1>
<div className="flex gap-2 mb-4">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="添加待办..."
className="flex-1 p-2 border rounded"
/>
<button
onClick={addTodo}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
添加
</button>
</div>
<ul className="space-y-2">
{todos.map(todo => (
<li
key={todo.id}
className={`p-3 border rounded flex items-center gap-2 ${
todo.completed ? 'bg-gray-100' : 'bg-white'
}`}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="w-4 h-4"
/>
<span className={todo.completed ? 'line-through text-gray-500' : ''}>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
className="ml-auto text-red-500"
>
删除
</button>
</li>
))}
</ul>
{todos.length === 0 && (
<p className="text-center text-gray-500 mt-8">
暂无待办事项
</p>
)}
</div>
);
}
export default TodoApp;
```
## 权限配置
在 `metadata.json` 中配置所需权限:
```json
{
"name": "我的应用",
"description": "应用描述",
"requestFramePermissions": [
"storage",
"diary",
"notes",
"notification"
]
}
```
常用权限:
- `storage` - 本地存储
- `diary` - 日记插件
- `notes` - 笔记插件
- `notification` - 通知
- `location` - 位置信息
- `camera` - 相机
- `microphone` - 麦克风
## 调试技巧
1. **使用 Mock 环境测试**
```html
<script src="/memento_mock.js"></script>
```
在浏览器中直接测试,无需启动 Memento 应用
2. **控制台调试**
```javascript
// 查看所有存储
Memento.utils.getStorageState()
// 清空存储
await Memento.utils.resetStorage()
// 查看设备信息
await Memento.system.getDeviceInfo()
```
3. **React DevTools**
配合 React DevTools 查看组件状态和 Memento 数据
4. **日志输出**
```typescript
Memento.utils.log('调试信息');
Memento.utils.error('错误信息');
```
## 注意事项
1. **Memento Mock 配置**
- 使用 `memento-mock` npm 包,而非外部脚本文件
- 在 index.tsx 中通过 `import.meta.env.DEV` 条件加载
- 生产环境不会加载 Mock 代码
2. **Memento.ready()**
- 所有 Memento API 调用前必须等待 `Memento.ready()`
- 可以多次调用,回调只执行一次
3. **Tailwind CSS 配置**
- 优先使用 npm 包而非 CDN
- Tailwind v4 使用 `@import "tailwindcss"` 语法
- 使用 `@theme` 定义主题变量
- 需要 PostCSS 配置 `@tailwindcss/postcss` 插件
4. **字体配置**
- 优先使用 `@fontsource` 包,避免外部字体请求
- 移除 Google Fonts 的 `<link>` 引用
- 在 CSS 中通过 `@import` 导入字体文件
5. **异步操作**
- 所有 Memento API 都是异步的,需要使用 `await` 或 `.then()`
6. **存储限制**
- 存储数据会被 JSON 序列化
- 不支持存储函数、Symbol 等特殊类型
- 注意存储空间限制
7. **类型安全**
- 使用 TypeScript 定义确保类型安全
- 调用插件时注意参数和返回值类型
8. **Mock 与生产环境**
- Mock 环境使用 localStorage
- 生产环境使用原生存储
- 通过环境变量控制 Mock 加载
## 执行步骤
当用户请求转换 React 应用时:
1. 读取并分析项目结构
2. 询问用户应用信息和需求
3. 配置 Memento Mock(安装 npm 包,更新 index.tsx)
4. 如需要,配置 Tailwind CSS(安装依赖,创建配置文件)
5. 如需要,配置本地字体(安装 @fontsource,更新 CSS)
6. 创建/更新 metadata.json
7. 创建 memento.d.ts 类型定义文件
8. 提供 Memento API 集成代码示例
9. 说明测试和部署注意事项
## 检查清单
转换完成后验证:
**Memento 配置:**
- [ ] package.json 包含 memento-mock 依赖
- [ ] index.tsx 包含条件加载 Mock 的代码
- [ ] metadata.json 配置正确
- [ ] memento.d.ts 类型定义存在
- [ ] Memento.ready() 在 API 调用前执行
- [ ] 开发环境能正常使用 Mock API
**Tailwind CSS 配置(如使用):**
- [ ] package.json 包含 tailwindcss、@tailwindcss/postcss、postcss、autoprefixer
- [ ] postcss.config.js 配置正确
- [ ] index.css 使用 @import "tailwindcss" 语法
- [ ] index.html 移除了 CDN 引用
**字体配置(如使用本地字体):**
- [ ] package.json 包含 @fontsource/* 字体包
- [ ] index.css 导入了本地字体
- [ ] index.html 移除了 Google Fonts 引用
**通用检查:**
- [ ] TypeScript 类型检查通过
- [ ] 开发构建正常
- [ ] 生产构建正常
- [ ] 生产环境不包含 Mock 代码Quick Install
$
npx ai-builder add skill hunmer/convert-react-to-mementoDetails
- Type
- skill
- Author
- hunmer
- Slug
- hunmer/convert-react-to-memento
- Created
- 5d ago