关于 telegram-mini-app-skill
面向 Web 开发者的框架无关指南,介绍 Telegram Mini Apps 的开发。内容涵盖 WebApp SDK、界面组件、主题、数据安全与测试。
主要功能
- 01WebApp SDK 加载与生命周期
- 02主题变量与主题变更
- 03导航和底部按钮 API
- 04触觉反馈、弹窗与提醒
- 05存储和设备 API
- 06安全、测试与调试
适用场景
- 01使用选定的 Web 框架启动 Mini App
- 02让应用适配 Telegram 主题和界面
- 03添加原生导航与按钮交互
- 04检查数据验证和测试实践
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rithprohos/ telegram-mini-app-skills --skill telegram-mini-app-skill安装后在 Claude Code 中输入/telegram-mini-app-skill确认已加载。
附带文件README.md
详细介绍
Telegram Mini App 开发指南
本指南介绍了如何开发 Telegram Mini App,不依赖特定框架。无论你使用 React、Vue、Svelte、Angular、Solid,还是原生 HTML/JS,本指南都会介绍通用的 SDK 概念、主题系统和最佳实践。
目录
- 什么是 Telegram Mini App?
- 加载 SDK
- WebApp 对象
- 初始化生命周期
- 主题与 CSS 变量
- 导航组件
- 底部按钮(MainButton 和 SecondaryButton)
- 触觉反馈
- 弹窗与提醒
- 数据验证与安全
- 存储 API
- 全屏模式
- 安全区域
- 可用事件
- 框架集成示例
- 设计指南
- 测试与调试
- 常见问题
1. 什么是 Telegram Mini App?
Telegram Mini App 是运行在 Telegram 即时通讯应用中的Web 应用(HTML、CSS、JavaScript)。它们可以通过以下方式启动:
- 机器人菜单——通过 BotFather 配置菜单按钮
- 内联按钮——机器人消息中的
web_app类型按钮 - 附件菜单——将机器人添加到附件菜单
- 直接链接——
t.me/botname/appname - 键盘按钮——
web_app类型的键盘按钮 - 主屏幕快捷方式——用户可以将 Mini App 添加到主屏幕
主要特点:
- 必须通过 HTTPS 提供服务
- 在所有平台(iOS、Android、桌面端、Web)上都通过 WebView 运行
- 可以访问 Telegram 用户数据、主题和原生 UI 组件
- 关联到通过 @BotFather 创建的机器人
2. 加载 SDK
选项 A:脚本标签(所有框架)
将以下内容放在 <head> 中,且必须位于其他所有脚本之前:
<script src="https://telegram.org/js/telegram-web-app.js"></script>
这会创建 window.Telegram.WebApp 对象,并自动注入用于主题设置的 CSS 变量。
选项 B:npm 包
适用于使用打包工具的现代 JavaScript 项目:
# Official community SDK (TypeScript-first, tree-shakeable)
npm install @telegram-apps/sdk
# Alternative lightweight wrapper
npm install @twa-dev/sdk
按框架加载
| 框架 | 加载方式 |
|---|---|
| 原生 HTML | 在 <head> 中添加 <script> 标签 |
| React / Next.js | 通过 next/script 使用 <Script src="..." strategy="beforeInteractive" />,或在 index.html 中添加 <script> |
| Vue / Nuxt | 在 index.html 中添加 <script>,或使用 useHead() 编写 Nuxt 插件 |
| Svelte / SvelteKit | 使用 <svelte:head> 标签或 app.html |
| Angular | 添加到 angular.json 的 scripts 数组或 index.html 中 |
3. WebApp 对象
加载 SDK 后,可通过以下位置访问主要 API:
const tg = window.Telegram.WebApp;
关键属性
| 属性 | 类型 | 描述 |
|---|---|---|
initData | string | 用于后端验证的原始初始化数据字符串 |
initDataUnsafe | object | 已解析的初始化数据(用户、聊天等)——未经验证,请勿在客户端信任 |
version | string | SDK 版本(例如,"8.0") |
platform | string | 平台标识符("android"、"ios"、"tdesktop"、"web"、"unknown") |
colorScheme | string | "light" 或 "dark" |
themeParams | object | 当前主题颜色,以键值对形式表示 |
isExpanded | boolean | Mini App 是否已展开至全高 |
viewportHeight | number | 当前视口高度,单位为像素 |
viewportStableHeight | number | 稳定的视口高度(不会随键盘显示而变化) |
headerColor | string | 当前标题栏颜色(#RRGGBB) |
backgroundColor | string | 当前背景颜色(#RRGGBB) |
bottomBarColor | string | 当前底栏颜色(#RRGGBB) |
isFullscreen | boolean | 是否已启用全屏模式 |
关键子对象
| 子对象 | 描述 |
|---|---|
BackButton | 控制标题栏中的返回按钮 |
MainButton | 控制底部主按钮 |
SecondaryButton | 控制底部次要按钮 |
SettingsButton | 控制上下文菜单中的设置项 |
HapticFeedback | 控制触觉反馈/振动反馈 |
CloudStorage | 云端键值存储(最多 1024 项,可在设备间同步) |
DeviceStorage | 持久化本地存储(最多 5 MB,仅限本设备) |
SecureStorage | 使用操作系统密钥链/密钥库的加密存储(最多 10 项) |
BiometricManager | 生物识别验证 |
Accelerometer | 加速度计传感器数据 |
Gyroscope | 陀螺仪传感器数据 |
DeviceOrientation | 设备方向数据 |
LocationManager | 位置访问 |
4. 初始化生命周期
每个 Mini App 都必须调用 tg.ready(),通知 Telegram 应用已准备好显示。
通用模式(适用于任何框架)
// 1. Get the WebApp instance
const tg = window.Telegram.WebApp;
// 2. Signal ready
tg.ready();
// 3. Expand to full height (recommended)
tg.expand();
// 4. Access user data
const user = tg.initDataUnsafe?.user;
console.log(user?.first_name, user?.username);
为什么 ready() 很重要
- 调用
ready()前,Telegram 会显示加载占位界面 - 应用初始 UI 渲染完成后,应尽早调用
- 在应用尚无可展示的有效内容前,不要调用
5. 主题与 CSS 变量
黄金法则
绝不要硬编码颜色。请使用 Telegram SDK 自动注入的 CSS 变量。这样可以确保应用在浅色模式、深色模式以及所有平台上都呈现原生观感。
可用的 CSS 变量
SDK 会自动在 <html> 元素上设置这些 CSS 变量:
| CSS 变量 | JS 属性(themeParams.X) | 描述 |
|---|---|---|
--tg-theme-bg-color | bg_color | 主背景 |
--tg-theme-text-color | text_color | 主要文本 |
--tg-theme-hint-color | hint_color | 次要文本/提示文本 |
--tg-theme-link-color | link_color | 链接 |
--tg-theme-button-color | button_color | 主按钮背景 |
--tg-theme-button-text-color | button_text_color | 按钮文本 |
--tg-theme-secondary-bg-color | secondary_bg_color | 次要背景(卡片、区块) |
--tg-theme-header-bg-color | header_bg_color | 页眉背景 |
--tg-theme-bottom-bar-bg-color | bottom_bar_bg_color | 底栏背景 |
--tg-theme-accent-text-color | accent_text_color | 强调文本 |
--tg-theme-section-bg-color | section_bg_color | 区块/卡片背景 |
--tg-theme-section-header-text-color | section_header_text_color | 区块标题文本 |
--tg-theme-section-separator-color | section_separator_color | 分隔线 |
--tg-theme-subtitle-text-color | subtitle_text_color | 副标题文本 |
--tg-theme-destructive-text-color | destructive_text_color | 危险操作文本 |
其他 CSS 变量
| CSS 变量 | 描述 |
|---|---|
--tg-color-scheme | "light" 或 "dark" |
--tg-viewport-height | 当前视口高度 |
--tg-viewport-stable-height | 稳定视口高度 |
--tg-safe-area-inset-top | 系统安全区域(顶部) |
--tg-safe-area-inset-bottom | 系统安全区域(底部) |
--tg-safe-area-inset-left | 系统安全区域(左侧) |
--tg-safe-area-inset-right | 系统安全区域(右侧) |
--tg-content-safe-area-inset-top | Telegram UI 安全区域(顶部) |
--tg-content-safe-area-inset-bottom | Telegram UI 安全区域(底部) |
--tg-content-safe-area-inset-left | Telegram UI 安全区域(左侧) |
--tg-content-safe-area-inset-right | Telegram UI 安全区域(右侧) |
使用示例
✅ 正确 — 使用 CSS 变量
/* CSS */
body {
background-color: var(--tg-theme-bg-color, #ffffff);
color: var(--tg-theme-text-color, #000000);
}
.card {
background-color: var(--tg-theme-section-bg-color, #f5f5f5);
border: 1px solid var(--tg-theme-section-separator-color, #e0e0e0);
}
.button-primary {
background-color: var(--tg-theme-button-color, #3b82f6);
color: var(--tg-theme-button-text-color, #ffffff);
}
.hint-text {
color: var(--tg-theme-hint-color, #999999);
}
始终提供回退值(
var()中的第二个参数),这样应用在开发期间脱离 Telegram 运行时也能正常工作。
❌ 错误 — 硬编码颜色
/* NEVER do this */
body {
background-color: #ffffff;
color: #000000;
}
.dark body {
background-color: #1a1a1a;
color: #ffffff;
}
创建简写变量系统(推荐)
为方便使用,可在全局 CSS 中将 Telegram 冗长的变量映射为较短的名称:
:root {
--bg: var(--tg-theme-bg-color, #ffffff);
--fg: var(--tg-theme-text-color, #171717);
--btn: var(--tg-theme-button-color, #3b82f6);
--btn-text: var(--tg-theme-button-text-color, #ffffff);
--hint: var(--tg-theme-hint-color, #a1a1aa);
--link: var(--tg-theme-link-color, #007aff);
--secondary-bg: var(--tg-theme-secondary-bg-color, #efeff4);
--section-bg: var(--tg-theme-section-bg-color, #ffffff);
--section-header: var(--tg-theme-section-header-text-color, #6d6d72);
--subtitle: var(--tg-theme-subtitle-text-color, #8e8e93);
--destructive: var(--tg-theme-destructive-text-color, #ff3b30);
--accent: var(--tg-theme-accent-text-color, #007aff);
--separator: var(--tg-theme-section-separator-color, #c8c7cc);
}
监听主题变化
tg.onEvent("themeChanged", () => {
// Theme params have been updated
// CSS variables are auto-updated by the SDK
// If using JS theming, re-read tg.themeParams
console.log("New color scheme:", tg.colorScheme);
});
6. 导航组件
BackButton
控制 Telegram 页眉中的原生返回按钮。请使用此按钮,而不是自行创建返回按钮。
// Show the back button
tg.BackButton.show();
// Handle clicks
tg.BackButton.onClick(() => {
// Navigate back in your app's routing
history.back(); // or your router's back method
});
// Hide when no longer needed
tg.BackButton.hide();
// Remove a specific handler
tg.BackButton.offClick(handler);
SettingsButton
在 Mini App 的上下文菜单(三点菜单)中添加“设置”选项。
tg.SettingsButton.show();
tg.SettingsButton.onClick(() => {
// Navigate to settings page
});
7. 底部按钮
Telegram 提供两个底部按钮:MainButton(主按钮)和 SecondaryButton(次要按钮)。它们固定显示在 Mini App 底部。
MainButton
const btn = tg.MainButton;
// Configure and show
btn.setText("SUBMIT ORDER");
btn.show();
// Or use setParams for multiple properties at once
btn.setParams({
text: "SUBMIT ORDER",
color: tg.themeParams.button_color,
text_color: tg.themeParams.button_text_color,
is_active: true,
is_visible: true,
has_shine_effect: false,
});
// Handle click
btn.onClick(() => {
// Perform the action
});
// Show loading state
btn.showProgress(true); // true = leave button active
btn.disable(); // prevent double-clicks
// Reset after completion
btn.hideProgress();
btn.enable();
btn.hide();
SecondaryButton
与 MainButton 的工作方式完全相同:
tg.SecondaryButton.setParams({
text: "CANCEL",
is_visible: true,
});
tg.SecondaryButton.onClick(() => {
// Handle secondary action
});
BottomButton 属性
| 属性 / 方法 | 描述 |
|---|---|
.text | 按钮文本 |
.color | 背景颜色 |
.textColor | 文本颜色 |
.isVisible | 按钮是否显示 |
.isActive | 按钮是否可点击 |
.isProgressVisible | 加载指示器是否显示 |
.setText(text) | 更新按钮文本 |
.show() / .hide() | 切换可见性 |
.enable() / .disable() | 切换可点击状态 |
.showProgress(leaveActive) | 显示加载指示器 |
.hideProgress() | 隐藏加载指示器 |
.setParams(params) | 一次设置多个属性 |
.onClick(cb) | 注册点击处理程序 |
.offClick(cb) | 移除点击处理程序 |
重要:清理处理程序
当点击处理程序不再适用时(例如组件卸载或页面导航时),务必将其移除。否则可能会导致幽灵处理程序。
8. 触觉反馈
触发原生触觉振动,提供触感反馈:
const haptic = tg.HapticFeedback;
// Impact feedback — for button presses, interactions
haptic.impactOccurred("light"); // light tap
haptic.impactOccurred("medium"); // medium tap
haptic.impactOccurred("heavy"); // strong tap
haptic.impactOccurred("rigid"); // rigid tap
haptic.impactOccurred("soft"); // soft tap
// Notification feedback — for results/outcomes
haptic.notificationOccurred("success"); // operation succeeded
haptic.notificationOccurred("error"); // operation failed
haptic.notificationOccurred("warning"); // caution
// Selection feedback — for selection changes
haptic.selectionChanged(); // e.g., picker/slider changes
适用场景
| 场景 | 类型 |
|---|---|
| 点击按钮 | impactOccurred("light") |
| 表单提交成功 | notificationOccurred("success") |
| 错误响应 | notificationOccurred("error") |
| 切换开关 | impactOccurred("medium") |
| 滑块/选择器的值发生变化 | selectionChanged() |
| 删除项目 | impactOccurred("heavy") |
| 清除/重置操作 | impactOccurred("light") |
9. 弹出窗口与警告框
使用 Telegram 原生对话框,而不是浏览器的 alert() / confirm():
// Simple alert
tg.showAlert("File saved successfully!", () => {
// Callback after user dismisses
});
// Confirmation dialog
tg.showConfirm("Are you sure you want to delete this?", (confirmed) => {
if (confirmed) {
// User tapped "OK"
}
});
// Custom popup with buttons
tg.showPopup(
{
title: "Choose an option", // optional
message: "What would you like to do?",
buttons: [
{ id: "save", type: "default", text: "Save" },
{ id: "delete", type: "destructive", text: "Delete" },
{ id: "cancel", type: "cancel" }, // text is auto-set
],
},
(buttonId) => {
switch (buttonId) {
case "save":
/* ... */ break;
case "delete":
/* ... */ break;
case "cancel":
/* ... */ break;
}
},
);
弹出窗口按钮类型
| 类型 | 行为 |
|---|---|
"default" | 常规按钮 |
"ok" | 显示“OK”文本,关闭弹出窗口 |
"close" | 显示“Close”文本,关闭弹出窗口 |
"cancel" | 显示“Cancel”文本,关闭弹出窗口 |
"destructive" | 红色/危险样式的按钮 |
10. 数据验证与安全
客户端数据
tg.initDataUnsafe 会提供解析后的用户数据,但这些数据可能被伪造。务必在后端进行验证。
// Client: send initData to your backend
const response = await fetch("/api/your-endpoint", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Telegram-Init-Data": tg.initData, // raw string for validation
},
body: JSON.stringify({
/* your payload */
}),
});
服务端验证(HMAC-SHA256)
后端必须使用机器人的令牌验证 initData:
1. Parse initData as query string
2. Extract `hash` parameter, keep remaining fields
3. Sort remaining fields alphabetically
4. Create data_check_string = sorted fields joined with '\n' in "key=value" format
5. secret_key = HMAC_SHA256(bot_token, "WebAppData")
6. Compare: HMAC_SHA256(data_check_string, secret_key) == hash
7. Check auth_date for freshness
伪代码:
data_check_string = "auth_date=<auth_date>\nquery_id=<query_id>\nuser=<user>"
secret_key = HMAC_SHA256(<bot_token>, "WebAppData")
if hex(HMAC_SHA256(data_check_string, secret_key)) == hash:
# Data is genuine and from Telegram
第三方验证(Ed25519 签名)
如果要与第三方共享数据(他们没有你的机器人令牌),请使用 signature 字段和 Telegram 的公钥:
- 生产环境:
e7bf03a2fa4602af4580703d88dda5bb59f32ed8b02a56c187fe7d34caed242d - 测试环境:
40055058a4ee38156a06562e52eece92a771bcd8346a8c4615cb7376eddf72ec
11. 存储 API
CloudStorage(跨设备同步)
最多可存储 1024 个键值对,每个值最多 4096 字节,通过 Telegram 服务器同步:
tg.CloudStorage.setItem("theme_pref", "dark", (err, success) => {});
tg.CloudStorage.getItem("theme_pref", (err, value) => {});
tg.CloudStorage.getItems(["key1", "key2"], (err, values) => {});
tg.CloudStorage.removeItem("theme_pref", (err, success) => {});
tg.CloudStorage.getKeys((err, keys) => {});
DeviceStorage(仅本地,Bot API 9.0+)
最多 5 MB,持久保存在设备上,类似于 localStorage:
tg.DeviceStorage.setItem("draft", longText);
tg.DeviceStorage.getItem("draft");
SecureStorage(加密,Bot API 9.0+)
最多存储 10 个条目,使用 OS Keychain(iOS)/Keystore(Android):
tg.SecureStorage.setItem("auth_token", token);
tg.SecureStorage.getItem("auth_token");
12. 全屏模式
自 Bot API 8.0(2024 年 11 月)起可用。允许 Mini App 使用整个屏幕:
// Enter full-screen
tg.requestFullscreen();
// Exit full-screen
tg.exitFullscreen();
// Check status
console.log(tg.isFullscreen);
// Listen for changes
tg.onEvent("fullscreenChanged", () => {
console.log("Fullscreen:", tg.isFullscreen);
});
tg.onEvent("fullscreenFailed", (event) => {
console.log("Fullscreen failed:", event.error);
});
重要:处于全屏模式时,必须处理安全区域,避免内容与系统 UI 元素(刘海、状态栏等)重叠。
13. 安全区域
有两种安全区域需要处理,在全屏模式下尤其重要:
系统安全区域(刘海、状态栏、主屏幕指示条)
.content {
padding-top: var(--tg-safe-area-inset-top, 0px);
padding-bottom: var(--tg-safe-area-inset-bottom, 0px);
padding-left: var(--tg-safe-area-inset-left, 0px);
padding-right: var(--tg-safe-area-inset-right, 0px);
}
内容安全区域(Telegram 自带的页眉、底栏)
强烈建议在 React/Next.js 等框架中避免将这些设置应用于原始文档 body,因为 Telegram 的 Webview 注入可能导致 100vw 或缩放限制,使内容被裁切或水平缩小到屏幕之外。
在所有 Webview 版本中,最稳妥的做法是采用 JavaScript 到 CSS 的混合回退方案。结合 contentSafeAreaInset.top(Telegram UI)和 safeAreaInset.top(移动设备刘海),即可完全避免重叠:
1. 在 Telegram 初始化循环中原生计算并合并内边距:
// Add base safeArea (device notches) + contentSafe (Telegram header ui) together
// Apply a hard fallback (e.g., 48px) just in case Telegram fails to inject variables entirely
const top = Math.max(
48,
(tg.contentSafeAreaInset?.top || 0) + (tg.safeAreaInset?.top || 0),
);
const bottom = Math.max(
32,
(tg.contentSafeAreaInset?.bottom || 0) + (tg.safeAreaInset?.bottom || 0),
);
// Explicitly inject safe bounds into your root HTML variables
document.documentElement.style.setProperty("--safe-top", `${top}px`);
document.documentElement.style.setProperty("--safe-bottom", `${bottom}px`);
2. 在 CSS 中(例如 globals.css 或 Tailwind 配置),引用注入的变量:
:root {
/* You can optionally add env() fallbacks here if you want native browser backup */
--safe-top: env(safe-area-inset-top, 48px);
--safe-bottom: env(safe-area-inset-bottom, 32px);
}
3. 最后,在 React 的各个页面包装器中设置内边距:
<main
style={{
// Add your desired extra padding (e.g., 1rem) to the base safe area boundary mathematically
paddingTop: "calc(1rem + var(--safe-top))",
paddingBottom: "calc(2rem + var(--safe-bottom))",
}}
>
{/* Content goes here safely below the Close/Settings notch */}
</main>
监听安全区域变化
tg.onEvent("safeAreaChanged", () => {
// System safe area changed (e.g., orientation change)
});
tg.onEvent("contentSafeAreaChanged", () => {
// Telegram UI safe area changed
});
14. 可用事件
使用 tg.onEvent(eventType, handler) 注册处理器,并使用 tg.offEvent(eventType, handler) 移除:
| 事件 | 触发条件 |
|---|---|
activated | Mini App 变为活动/可见状态 |
deactivated | Mini App 变为非活动/隐藏状态 |
themeChanged | 用户更改了 Telegram 主题 |
viewportChanged | 视口高度发生变化(键盘、展开) |
safeAreaChanged | 系统安全区域内边距发生变化 |
contentSafeAreaChanged | Telegram 内容安全区域发生变化 |
mainButtonClicked | 主按钮被按下 |
secondaryButtonClicked | 次要按钮被按下 |
backButtonClicked | 返回按钮被按下 |
settingsButtonClicked | 设置按钮被按下 |
invoiceClosed | 支付账单已关闭 |
popupClosed | 弹出窗口已关闭 |
qrTextReceived | 已扫描二维码 |
clipboardTextReceived | 已接收剪贴板文本 |
writeAccessRequested | 写入权限请求结果 |
contactRequested | 联系人共享结果 |
fullscreenChanged | 全屏状态发生变化 |
fullscreenFailed | 全屏请求失败 |
homeScreenAdded | 应用已添加到主屏幕 |
homeScreenChecked | 已检查主屏幕状态 |
biometricManagerUpdated | 生物识别管理器状态发生变化 |
biometricAuthRequested | 生物识别身份验证结果 |
shareMessageSent | 分享消息已发送 |
shareMessageFailed | 分享消息发送失败 |
emojiStatusSet | 已设置表情状态 |
emojiStatusFailed | 设置表情状态失败 |
fileDownloadRequested | 文件下载请求结果 |
15. 框架集成示例
原生 JavaScript
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--tg-theme-bg-color, #fff);
color: var(--tg-theme-text-color, #000);
}
.card {
background: var(--tg-theme-section-bg-color, #f5f5f5);
border-radius: 14px;
padding: 16px;
margin: 16px;
}
.hint {
color: var(--tg-theme-hint-color, #999);
font-size: 13px;
}
</style>
</head>
<body>
<div class="card">
<h1>Hello, <span id="username">User</span>!</h1>
<p class="hint">Welcome to the Mini App</p>
</div>
<script>
const tg = window.Telegram.WebApp;
tg.ready();
tg.expand();
const user = tg.initDataUnsafe?.user;
if (user) {
document.getElementById("username").textContent = user.first_name;
}
tg.MainButton.setText("DONE");
tg.MainButton.show();
tg.MainButton.onClick(() => {
tg.HapticFeedback.notificationOccurred("success");
tg.close();
});
</script>
</body>
</html>
React / Next.js
// hooks/useTelegram.ts — Reusable hook
import { useEffect, useState, useCallback } from "react";
export function useTelegram() {
const [tg, setTg] = useState<TelegramWebApp | null>(null);
const [user, setUser] = useState<TelegramUser | null>(null);
useEffect(() => {
if (typeof window !== "undefined" && window.Telegram?.WebApp) {
const webapp = window.Telegram.WebApp;
webapp.ready();
webapp.expand();
setTg(webapp);
setUser(webapp.initDataUnsafe?.user || null);
}
}, []);
const showMainButton = useCallback(
(text: string, onClick: () => void) => {
if (!tg) return;
tg.MainButton.setText(text);
tg.MainButton.show();
tg.MainButton.onClick(onClick);
return () => tg.MainButton.offClick(onClick);
},
[tg],
);
const showBackButton = useCallback(
(onClick: () => void) => {
if (!tg) return;
tg.BackButton.show();
tg.BackButton.onClick(onClick);
return () => {
tg.BackButton.offClick(onClick);
tg.BackButton.hide();
};
},
[tg],
);
return { tg, user, showMainButton, showBackButton };
}
// pages/MyPage.tsx
"use client";
import { useEffect } from "react";
import { useTelegram } from "@/hooks/useTelegram";
export default function MyPage() {
const { tg, user, showBackButton } = useTelegram();
useEffect(() => {
return showBackButton?.(() => window.history.back());
}, [showBackButton]);
return (
<main style={{ background: "var(--tg-theme-secondary-bg-color)" }}>
<h1 style={{ color: "var(--tg-theme-text-color)" }}>
Hello, {user?.first_name}!
</h1>
</main>
);
}
Next.js 布局设置
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Script
src="https://telegram.org/js/telegram-web-app.js"
strategy="beforeInteractive"
/>
{children}
</body>
</html>
);
}
suppressHydrationWarning是必需的,因为 Telegram 的脚本会在 SSR/水合期间向 DOM 注入样式属性。
Vue 3 / Nuxt
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const tg = ref<any>(null);
const user = ref<any>(null);
export function useTelegram() {
onMounted(() => {
if (window.Telegram?.WebApp) {
tg.value = window.Telegram.WebApp;
tg.value.ready();
tg.value.expand();
user.value = tg.value.initDataUnsafe?.user || null;
}
});
function showBackButton(onClick: () => void) {
tg.value?.BackButton.show();
tg.value?.BackButton.onClick(onClick);
onUnmounted(() => {
tg.value?.BackButton.offClick(onClick);
tg.value?.BackButton.hide();
});
}
return { tg, user, showBackButton };
}
</script>
<template>
<main :style="{ background: 'var(--tg-theme-secondary-bg-color)' }">
<h1 :style="{ color: 'var(--tg-theme-text-color)' }">
Hello, {{ user?.first_name }}!
</h1>
</main>
</template>
<script setup lang="ts">
import { useTelegram } from "@/composables/useTelegram";
import { useRouter } from "vue-router";
const { user, showBackButton } = useTelegram();
const router = useRouter();
showBackButton(() => router.back());
</script>
Svelte / SvelteKit
<script context="module" lang="ts">
export function getTelegram() {
if (typeof window !== 'undefined' && window.Telegram?.WebApp) {
return window.Telegram.WebApp;
}
return null;
}
</script>
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { getTelegram } from '$lib/telegram';
let user: any = null;
onMount(() => {
const tg = getTelegram();
if (tg) {
tg.ready();
tg.expand();
user = tg.initDataUnsafe?.user;
tg.BackButton.show();
tg.BackButton.onClick(() => history.back());
}
});
onDestroy(() => {
const tg = getTelegram();
tg?.BackButton.hide();
});
</script>
<main style="background: var(--tg-theme-secondary-bg-color); color: var(--tg-theme-text-color);">
<h1>Hello, {user?.first_name ?? 'User'}!</h1>
</main>
SvelteKit 布局
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
%sveltekit.head%
</head>
<body>
%sveltekit.body%
</body>
</html>
16. 设计指南
Telegram 官方为 Mini Apps 制定的设计原则:
核心原则
- 移动优先:所有元素都必须响应式适配,并针对移动设备视口进行优化
- 原生体验:模仿现有 Telegram UI 组件的样式和行为
- 流畅动画:所有动画的目标帧率为 60fps
- 无障碍性:所有输入框和图像都应有标签
- 主题适配:所有颜色都使用 Telegram CSS 变量
- 安全区域适配:遵循安全区域要求,尤其是在全屏模式下
- 注重性能:在 Android 上,检查 User-Agent 以判断设备性能等级,并在低端设备上减少动画
推荐的 UI 模式
| 元素 | 推荐样式 |
|---|---|
| 页面背景 | var(--tg-theme-secondary-bg-color) |
| 卡片 / 区块 | var(--tg-theme-section-bg-color),并使用 border-radius: 14px |
| 区块标签 | 大写、小号字体,使用 var(--tg-theme-section-header-text-color) |
| 提示文本 | 较小字体,使用 var(--tg-theme-hint-color) |
| 分隔线 | 1px 线条,使用 var(--tg-theme-section-separator-color) |
| 输入框字体大小 | ≥ 16px(防止 iOS 在聚焦时自动缩放) |
| 按钮 | 主要操作使用 MainButton,或使用 var(--tg-theme-button-color) 自定义按钮 |
| 危险操作 | var(--tg-theme-destructive-text-color) |
| 底部内边距 | 添加足够的内边距,以避免与 MainButton 重叠(约 80px) |
17. 测试与调试
开发环境
- 运行开发服务器,地址为
localhost - 通过 HTTPS 隧道公开:
ngrok http 3000(或使用 Cloudflare Tunnel、localtunnel 等) - 配置 BotFather:
- 创建或选择你的机器人
- 通过
/setmenubutton或/newapp设置 Web App URL
- 在 Telegram 中打开并进行测试
调试技巧
| 平台 | 方法 |
|---|---|
| Android | 在 Android 开发者设置中启用 WebView 调试 → chrome://inspect |
| iOS | Safari → 开发 → 你的设备 → 你的 WebView |
| 桌面端 | 在 Mini App 内右键单击 → “检查元素”(部分版本支持) |
| Telegram Web | 使用常规浏览器开发者工具 |
在 Telegram 外部测试
对所有 tg 调用进行安全检查包装。为开发环境提供备用 UI:
const tg = window.Telegram?.WebApp;
const isInTelegram = !!tg?.initData;
if (isInTelegram) {
tg.ready();
tg.MainButton.show();
} else {
// Show a regular HTML button instead for local testing
console.log("Running outside Telegram — showing fallback UI");
}
18. 常见问题
| 问题 | 解决方案 |
|---|---|
| Telegram 中的颜色显示不正确 | 使用 --tg-theme-* CSS 变量,切勿硬编码 |
| 应用卡在加载动画界面 | 初始渲染后调用 tg.ready() |
| MainButton 无响应 | 注册新处理程序前,务必先调用 offClick(handler) |
| iOS 在输入框获得焦点时放大页面 | 将输入框的 font-size 设为 ≥ 16px |
| SSR 水合错误(Next.js、Nuxt) | 为 <html> 标签添加 suppressHydrationWarning |
| initData 为空 | 应用是通过键盘按钮启动的(符合预期)——妥善处理 |
| API 调用返回 403 | 在服务端正确验证 initData;检查 auth_date 是否在有效期内 |
| 按钮处理程序中的状态过时 | 使用 refs(React)或响应式 getter(Vue)——处理程序会捕获闭包变量 |
| 内容被 MainButton 遮挡 | 为主要内容添加底部内边距(约 80–100px) |
| 全屏模式下安全区域内容重叠 | 使用 --tg-safe-area-inset-* 和 --tg-content-safe-area-inset-* CSS 变量 |
window.Telegram 未定义 | SDK 脚本尚未加载——确保它在应用代码之前加载 |
| MainButton 被双击 | 点击后立即禁用按钮并显示进度 |
| 主题未动态更新 | 监听 themeChanged 事件;CSS 变量会自动更新 |
| Clipboard API 无法使用 | 使用 tg.readTextFromClipboard(),而不是浏览器的 Clipboard API |
| Web 端文件下载无法使用 | 在响应标头中包含 Content-Disposition 和 Access-Control-Allow-Origin: https://web.telegram.org |
快速参考:实用方法
const tg = window.Telegram.WebApp;
// Basics
tg.ready(); // Signal app is ready
tg.expand(); // Expand to full height
tg.close(); // Close the Mini App
// User data
tg.initData; // Raw string for server validation
tg.initDataUnsafe.user; // User object (not validated!)
// Navigation
tg.openLink(url); // Open URL in browser
tg.openLink(url, { try_instant_view: true }); // Try Instant View
tg.openTelegramLink(url); // Open telegram link (resolves inside Telegram)
// Sharing
tg.switchInlineQuery(query, ["users", "groups"]); // Switch to inline mode
tg.shareToStory(mediaUrl, params); // Share to Stories
// Clipboard
tg.readTextFromClipboard((text) => {}); // Read clipboard
// QR Code
tg.showScanQrPopup({ text: "Scan QR" }, (data) => {
// Handle QR data
return true; // return true to close popup
});
tg.closeScanQrPopup();
// Payments
tg.openInvoice(invoiceUrl, (status) => {
// status: "paid", "cancelled", "failed", "pending"
});
// Colors
tg.setHeaderColor("#RRGGBB"); // Custom header color
tg.setBackgroundColor("#RRGGBB"); // Custom background color
tg.setBottomBarColor("#RRGGBB"); // Custom bottom bar color
// Permissions
tg.requestWriteAccess((granted) => {}); // Request PM write access
tg.requestContact((shared) => {}); // Request phone contact
// Home screen
tg.addToHomeScreen(); // Prompt add to home screen
tg.checkHomeScreenStatus((status) => {}); // 'added', 'missed', etc.
// Fullscreen
tg.requestFullscreen();
tg.exitFullscreen();
// Closing confirmation
tg.enableClosingConfirmation(); // Warn user before closing
tg.disableClosingConfirmation();
// Send data to bot (limited to 4096 bytes)
tg.sendData(JSON.stringify({ action: "submit" }));