快速开发应用
快速开发一个应用
本文将指导您从零开始快速搭建一个最小化的 GMSSH 应用,不依赖预设模板,让您深入理解应用的基本结构和开发流程。
适用场景
- 希望完全掌控项目结构的开发者
- 需要自定义配置的复杂应用
- 学习 GMSSH 应用开发原理
前置准备
1. 创建应用
首先需要在开发者中心创建一个新应用,并选择空项目模板:
创建步骤
- 登录 开发者中心
- 点击「创建新应用」
- 选择「空项目」模板
- 填写应用基本信息
如果您还不熟悉应用创建流程,请先查看 创建应用指南。
2. 项目目录结构
应用创建完成后,您需要手动构建项目目录结构。这里我后端选择 Python 来演示,推荐的标准结构如下:
项目根目录/
├── backend/ # 后端代码目录
│ ├── build.sh # 后端构建脚本
│ ├── main.py # 主程序入口文件
│ └── requirements.txt # Python依赖配置文件
├── web/ # 前端代码目录
│ ├── build.sh # 前端构建脚本
│ └── index.html # 前端入口页面
├── .gitignore # Git忽略文件配置
└── makefile # 项目构建配置文件
核心代码实现
后端服务实现
为了满足不同开发者的技术栈偏好,我们提供了多种编程语言的后端服务实现示例。每个示例都实现了相同的 API 接口,您可以根据自己的技术背景选择合适的实现方案。
选择建议
- Python:适合快速原型开发和数据处理场景
- Go:适合高性能和高并发需求的应用
Python 实现(Flask)
基于 Flask 框架的轻量级实现,适合快速开发和原型验证:
点击查看 Python 完整代码
文件路径: backend/main.py
import socket
from flask import Flask, jsonify
app = Flask(__name__)
@app.post("/hello")
def hello():
return jsonify(code=200,data="hello world!", msg="OK")
@app.get("/ping")
def ping():
return jsonify(code=200, data="ping", msg="OK")
def get_free_port():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0)) # 端口 0 让操作系统自动分配
return s.getsockname()[1]
if __name__ == "__main__":
# port = get_free_port()
# 为了方便调试,直接指定端口,生产环境使用 get_free_port()
app.run(host="0.0.0.0", port=8899, debug=True)
运行方式:
# 安装依赖
pip install flask
# 启动服务
python main.py
Go 实现(Gin)
基于 Gin 框架的高性能实现,适合生产环境和高并发场景:
点击查看 Go 完整代码
文件路径: backend/main.go
package main
import (
"fmt"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"net"
"net/http"
"os"
"os/signal"
"syscall"
)
// 测试服务可以使用这个文件地址
const PORT_FILE = "app.port"
// 开发完成上架必须使用这个地址,根据您的应用名称来进行替换
// const PORT_FILE = "/.__gmssh/plugin/{组织名}/{应用名}/tmp/app.port"
func getFreePort() (int, error) {
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
return 0, err
}
defer listener.Close()
addr := listener.Addr().(*net.TCPAddr)
return addr.Port, nil
}
// 将端口号写入文件
func writePortToFile(port int) error {
return os.WriteFile(PORT_FILE, []byte(fmt.Sprintf("%d", port)), 0644)
}
// 删除端口文件
func removePortFile() {
// 检查文件是否存在
if _, err := os.Stat(PORT_FILE); err == nil {
// 文件存在,删除它
if err := os.Remove(PORT_FILE); err != nil {
fmt.Printf("删除端口文件失败: %v\n", err)
} else {
fmt.Println("端口文件已成功删除")
}
}
}
// 设置信号处理
func setupSignalHandler() {
c := make(chan os.Signal, 1)
// 监听多种信号: SIGINT (Ctrl+C), SIGTERM (终止), SIGHUP (挂起)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
// 注意接受到-9的信号无法捕获
go func() {
sig := <-c
fmt.Printf("接收到信号: %s, 正在清理资源...\n", sig)
removePortFile()
os.Exit(0)
}()
}
func main() {
// 设置信号处理,确保在程序退出时删除端口文件
setupSignalHandler()
// 确保在程序panic时也能删除端口文件
defer func() {
if r := recover(); r != nil {
fmt.Printf("程序发生panic: %v, 正在清理资源...\n", r)
removePortFile()
panic(r) // 重新抛出panic
}
}()
r := gin.Default()
// POST /hello
r.POST("/hello", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": "hello world!",
"msg": "OK",
})
})
// GET /ping
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": "ping",
"msg": "OK",
})
})
// port, err := getFreePort()
// if err != nil {
// panic(fmt.Sprintf("获取空闲端口失败: %v", err))
// }
// 设置端口号
port := 8899
// 写入端口文件
if err := writePortToFile(port); err != nil {
panic(fmt.Sprintf("将端口号写入文件失败: %v", err))
}
fmt.Printf("端口号 %d 已写入文件\n", port)
addr := fmt.Sprintf(":%d", port)
// 启动服务
fmt.Printf("服务器启动在端口: %d\n", port)
if err := r.Run(addr); err != nil {
// 如果服务器启动失败,也需要删除端口文件
removePortFile()
panic(fmt.Sprintf("服务器启动失败: %v", err))
}
// 正常退出时也删除端口文件(虽然通常不会执行到这里)
removePortFile()
}
运行方式:
# 初始化模块
go mod init backend
# 安装依赖
go get github.com/gin-gonic/gin
go get github.com/gin-contrib/cors
# 启动服务
go run main.go
API 接口说明
两个实现都提供了相同的 API 接口:
POST /hello
- 返回问候消息GET /ping
- 健康检查接口
所有接口都返回统一的 JSON 格式:{"code": 200, "data": "...", "msg": "OK"}
前端代码示例
html项目模板
点击查看完整前端代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>GM SDK Demo</title>
<!-- 1. 引入 GM 提供的 SDK -->
<script>
(function () {
// 设置当前域名 不包含二级域名
document.domain = window.location.hostname.split('.').slice(-2).join('.');
// 私有状态存储
const state = {
resizePadding: 6,
width: 0,
height: 0,
listeners: new Set(),
gmcCallback: null,
serveActiveCallback: null,
};
// 获取url参数
const getURLParams = (url) => {
return window.location.search.slice(1).split('&').reduce((acc, cur) => {
const [key, value] = cur.split('=');
acc[key] = value;
return acc;
}, {});
}
// 初始化 $gm 对象
window.$gm = { ...window.parent.$gm, ...getURLParams(window.location.href) } || {};
// 消息类型处理器映射
const messageHandlers = {
// init: (payload) => {
// window.$gm={...window.$gm,...payload}
// },
resize: (payload) => {
state.width = payload.width;
state.height = payload.height;
state.listeners.forEach((fn) =>
fn({ width: payload.width, height: payload.height }),
);
},
gmcListener: (payload) => {
if (state.gmcCallback) {
state.gmcCallback(payload);
}
},
serveListener: (payload) => {
if (state.serveActiveCallback) {
state.serveActiveCallback(payload);
}
},
};
// 统一消息处理函数
function handleMessageEvent(e) {
try {
const data = e.data || {};
if (!data.type || !messageHandlers[data.type]) {
return;
}
messageHandlers[data.type](data.data);
} catch (error) {
console.error('Message handling error:', error);
}
}
// 注册全局消息监听器(仅此一个)
window.addEventListener('message', handleMessageEvent);
// ====== API 接口实现 ======
// 监听 resize 消息
window.$gm.childRectListener = (callback) => {
if (callback) {
state.listeners.add(callback);
}
return () => callback && state.listeners.delete(callback);
};
// 监听 GMC 消息
window.$gm.mainGMCListener = (callback) => {
state.gmcCallback = callback;
};
// 监听 GMC 消息
window.$gm.serveActiveListener = (callback) => {
state.serveActiveCallback = callback;
};
// ====== 保持不变的鼠标监听逻辑 ======
document.onmousemove = (e) => {
const inResizeZone =
e.clientX < state.resizePadding ||
e.clientY < state.resizePadding ||
e.clientX > window.innerWidth - state.resizePadding ||
e.clientY > window.innerHeight - state.resizePadding;
const currentState = inResizeZone ? 'resizeStart' : 'resizeEnd';
window.parent.postMessage(
{ type: currentState, data: window.$gm.fileId },
'*',
);
};
document.addEventListener('mousedown', () => {
window.parent.postMessage(
{ type: 'iframeMouseDown', data: window.$gm.fileId },
'*',
);
})
// ====== 工具方法 ======
// 获取窗口尺寸
window.$gm.getRectSize = () => ({
width: state.width,
height: state.height,
});
// 关闭应用方法
window.$gm.closeApp = function () {
if (window.$gm.fileId) {
window.parent.postMessage(
{ type: 'closeApp', data: window.$gm.fileId },
'*',
);
}
};
// 发送消息至父级
window.$gm.emitParent = function (msg) {
window.parent.postMessage(
msg,
'*',
);
};
// 应用初始化方法
window.$gm.init = function () {
return window.$gm
.request('/api/center/check_status', {
method: 'post',
data: {
app_name: window?.$gm?.name,
version: window?.$gm?.version,
communication_type: window?.$gm?.communicationType,
},
})
};
// 设置主题
const style = document.createElement('style');
style.textContent = window.$gm.themeCss || '';
document.head.appendChild(style);
document.documentElement.setAttribute('data-theme', 'dark');
})();
</script>
<style>
body {
background-color: #1a1a1a;
color: white;
font-family: Arial, sans-serif;
padding: 20px;
}
button {
background-color: #333;
color: white;
border: 1px solid #555;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 10px 0;
}
button:hover {
background-color: #555;
}
pre {
background-color: #2a2a2a;
color: white;
padding: 15px;
border-radius: 5px;
border: 1px solid #444;
white-space: pre-wrap;
word-wrap: break-word;
margin-top: 15px;
}
</style>
</head>
<body>
<h2>请求示例</h2>
<button id="btn">点我发请求</button>
<pre id="result"></pre>
<script>
// 2. 点击按钮后调用后端接口
document.getElementById("btn").addEventListener("click", () => {
// 使用 GM 封装的 axios 实例发起 POST 请求
$gm
.request("/api/call/example/demo/hello", {
method: "POST",
data: {
"version": "1.0.0",
"transport": "http",
"params": {
"lang": "zh-CN"
}
}, // 发送 JSON 数据作为请求体
})
.then((res) => {
// 3. 打印返回结果
document.getElementById("result").textContent = JSON.stringify(res, null, 2);
})
.catch((err) => {
document.getElementById("result").textContent = "请求失败:\n" + err;
});
});
</script>
</body>
</html>
vue项目模板
点击查看完整前端代码示例
<template>
<h2>请求示例</h2>
<button id="btn">点我发请求</button>
<pre id="result"></pre>
</template>
<script setup>
// 2. 点击按钮后调用后端接口
document.getElementById("btn").addEventListener("click", () => {
// 使用 GM 封装的 axios 实例发起 POST 请求
$gm
.request("/api/call/example/demo/hello", {
method: "POST",
data: {
"version": "1.0.0",
"transport": "http",
"params": {
"lang": "zh-CN"
}
}, // 发送 JSON 数据作为请求体
})
.then((res) => {
// 3. 打印返回结果
document.getElementById("result").textContent = JSON.stringify(res, null, 2);
})
.catch((err) => {
document.getElementById("result").textContent = "请求失败:\n" + err;
});
});
</script>
<style scoped>
body {
background-color: #1a1a1a;
color: white;
font-family: Arial, sans-serif;
padding: 20px;
}
button {
background-color: #333;
color: white;
border: 1px solid #555;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 10px 0;
}
button:hover {
background-color: #555;
}
pre {
background-color: #2a2a2a;
color: white;
padding: 15px;
border-radius: 5px;
border: 1px solid #444;
white-space: pre-wrap;
word-wrap: break-word;
margin-top: 15px;
}
</style>
react项目模板
点击查看完整前端代码示例
import './App.css'
function App() {
// 2. 点击按钮后调用后端接口
document.getElementById("btn").addEventListener("click", () => {
// 使用 GM 封装的 axios 实例发起 POST 请求
$gm
.request("/api/call/example/demo/hello", {
method: "POST",
data: {
"version": "1.0.0",
"transport": "http",
"params": {
"lang": "zh-CN"
}
}, // 发送 JSON 数据作为请求体
})
.then((res) => {
// 3. 打印返回结果
document.getElementById("result").textContent = JSON.stringify(res, null, 2);
})
.catch((err) => {
document.getElementById("result").textContent = "请求失败:\n" + err;
});
});
return (
<>
<div>
<h2>请求示例</h2>
<button id="btn">点我发请求</button>
<pre id="result"></pre>
</div>
</>
)
}
export default App
目录说明
运行与测试
本地开发调试
快速启动
- 安装依赖:
cd backend && pip install -r requirements.txt
- 启动后端:
python backend/main.py
- 访问应用:通过开发者工具访问您的应用
发布前检查
在发布应用前,请确保:
- ✅ 应用详情信息完整
- ✅ 前后端
build.sh
脚本可正常构建 - ✅ 所有功能测试通过
- ✅ 代码符合平台规范
注意事项
- 确保构建脚本正常运行,否则可能导致应用无法上架
- 建议在发布前进行充分的功能测试
- 生产环境部署时需要配置适当的服务器环境
下一步
恭喜!您已经成功创建了一个基础的 GMSSH 应用。接下来您可以使用高级的方式sdk 来开发应用了