开发应用
应用开发主要包含以下步骤:
- 开发并编译应用后端服务
- 使用 web 前端技术开发应用前端界面
- 使用开发者工具
ugcli创建应用项目 - 编写应用配置文件
project.yaml - 拷贝应用后端服务可执行文件及前端文件到应用项目对应目录下
- 制作应用图标并拷贝到应用项目对应目录下
- 使用开发者工具
ugcli打包应用
下面我们将通过从零开始创建一个简单的示例应用,介绍如何快速开发应用并打包发布。你将快速学习应用开发的基本流程和开发者工具 ugcli 的基本使用。
开发后端服务
UGOS Pro 应用的后端服务目前仅支持使用 C/C++、Go 等编译型语言开发,需要编译生成可在 Linux 系统上直接运行的可执行文件。
本示例中,后端服务使用 Go 语言开发,实现一个简单的 HTTP 服务,提供心跳接口。
创建后端服务目录 backend,并编写 main.go 文件:
点击查看代码
package main
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"time"
)
// HeartbeatResponse 定义心跳响应结构
type HeartbeatResponse struct {
Timestamp string `json:"timestamp"`
Status string `json:"status"`
}
// heartbeatHandler 处理心跳请求
func heartbeatHandler(w http.ResponseWriter, r *http.Request) {
// 只允许GET方法
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 设置响应头
w.Header().Set("Content-Type", "application/json")
// 创建响应数据
response := HeartbeatResponse{
Timestamp: time.Now().Format(time.DateTime),
Status: "healthy",
}
// 编码JSON响应
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
}
func main() {
// 定义命令行参数
port := flag.Int("port", 21010, "Port to listen on")
flag.Parse()
// 注册路由
http.HandleFunc("/api/heartbeat", heartbeatHandler)
// 构建监听地址
addr := fmt.Sprintf(":%d", *port)
// 启动服务
fmt.Printf("Server starting on %s\n", addr)
if err := http.ListenAndServe(addr, nil); err != nil {
fmt.Printf("Server failed to start: %v\n", err)
}
}运行 go build -o myapp_serv 编译,生成后端服务可执行文件 myapp_serv。
最终 backend 的目录结构如下:
backend/
├── go.mod
├── main.go
└── myapp_serv开发前端页面
你可以自由选择前端框架,如 Vue、React 等,开发你的应用界面。完成开发后,需要将构建产物(HTML、CSS、JS 等)放入应用包中的 www 目录即可。
本示例应用的前端页面将使用 HTML + CSS + JavaScrip 开发, 实现一个简单的用户界面,用于展示后端服务的心跳状态。
创建 www/index.html 文件,内容如下:
点击查看代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyApp</title>
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="container">
<h1>MyApp 状态监控</h1>
<div class="auto-start">页面加载后自动开始监控...</div>
<div id="status" class="status disconnected">
<div class="status-indicator"></div>
<span>初始化中...</span>
</div>
<div class="response-display" id="responseDisplay">
监控数据将显示在这里...
</div>
</div>
<script src="./js/app.js"></script>
</body>
</html>创建 www/js/app.js 文件,内容如下:
点击查看代码
let heartbeatInterval = null;
let requestCount = 0;
let isAutoStarted = false;
let maxEntries = 2;
// 更新状态显示
function updateStatus(connected, message) {
const statusElement = document.getElementById('status');
const statusText = statusElement.querySelector('span');
if (connected) {
statusElement.className = 'status connected';
statusText.textContent = message || '连接正常';
} else {
statusElement.className = 'status disconnected';
statusText.textContent = message || '连接断开';
}
}
// 格式化时间显示
function formatTime(dateString) {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
// 发送心跳请求
async function sendHeartbeat() {
const apiUrl = "/api/heartbeat";
const responseDisplay = document.getElementById('responseDisplay');
requestCount++;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
// 更新响应显示
const newEntry = `请求 #${requestCount} - ${new Date().toLocaleTimeString()}\n` +
JSON.stringify(data, null, 2) + '\n\n';
// 保持响应显示区域内容简洁,只显示最近记录
const currentContent = responseDisplay.textContent;
const entries = currentContent.split('\n\n').filter(entry => entry.trim());
entries.unshift(newEntry.trim());
// 只保留最近3条记录
if (entries.length > maxEntries) {
entries.length = maxEntries;
}
responseDisplay.textContent = entries.join('\n\n');
updateStatus(true, `监控中 - 最后更新: ${new Date().toLocaleTimeString()}`);
} catch (error) {
// 更新响应显示
const newEntry = `请求 #${requestCount} - ${new Date().toLocaleTimeString()}\n` +
`错误: ${error.message}\n\n`;
const currentContent = responseDisplay.textContent;
const entries = currentContent.split('\n\n').filter(entry => entry.trim());
entries.unshift(newEntry.trim());
if (entries.length > maxEntries) {
entries.length = maxEntries;
}
responseDisplay.textContent = entries.join('\n\n');
updateStatus(false, `连接错误: ${error.message}`);
}
}
// 开始心跳测试
function startHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
// 立即发送一次请求
sendHeartbeat();
// 设置定时器,每秒发送一次请求
heartbeatInterval = setInterval(sendHeartbeat, 1000);
updateStatus(true, '监控已开始...');
}
// 停止心跳测试
function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
updateStatus(false, '监控已暂停');
}
// 页面加载完成后自动开始监控
document.addEventListener('DOMContentLoaded', function () {
// 自动开始监控
setTimeout(() => {
startHeartbeat();
isAutoStarted = true;
}, 10);
});创建 www/css/style.css 文件,内容如下:
点击查看代码
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.status {
display: flex;
align-items: center;
margin-bottom: 20px;
padding: 10px;
border-radius: 4px;
}
.status.connected {
background-color: #d4edda;
color: #155724;
}
.status.disconnected {
background-color: #f8d7da;
color: #721c24;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
}
.status.connected .status-indicator {
background-color: #28a745;
}
.status.disconnected .status-indicator {
background-color: #dc3545;
}
.response-display {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 15px;
margin-top: 20px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
min-height: 100px;
max-height: 300px;
overflow-y: auto;
}
.timestamp {
font-size: 1.2em;
font-weight: bold;
color: #007bff;
text-align: center;
margin: 20px 0;
}
.controls {
text-align: center;
margin: 20px 0;
}
button {
padding: 10px 20px;
margin: 0 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.start-btn {
background-color: #007bff;
color: white;
}
.stop-btn {
background-color: #dc3545;
color: white;
}
.config {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.auto-start {
text-align: center;
color: #666;
font-style: italic;
margin-bottom: 10px;
}最终 www 的目录结构如下:
www
├── css
│ └── style.css
├── index.html
└── js
└── app.jscss/js 文件浏览器缓存处理
系统 web 服务器在响应 css/js 文件时会设置 Cache-Control 头允许浏览器缓存。 避免应用新版本发布后,用户浏览器仍然使用旧版本的文件,推荐采用如下其中一种方式处理:
- 文件名哈希,如
app.a1b2c3d4.js,修改文件内容时同时修改文件名 - 查询参数携带版本号,如
app.js?v=1.0.1,发布新版本时更新版本号
创建应用项目
运行 ugcli create com.mycompany.myapp 创建应用项目,完成创建后将会在当前目录下生成 com.mycompany.myapp 目录,结构如下:
com.mycompany.myapp
├── project.yaml # 应用配置文件
├── rootfs_amd64 # x86_64 架构的应用文件
│ └── bin
├── rootfs_arm64 # arm64 架构的应用文件
│ └── bin
├── rootfs_common # 两个架构共用的应用文件
│ ├── icon.png # 应用图标
│ └── www # 应用前端文件编写应用配置文件
修改应用项目根目录下的 project.yaml 文件,配置应用的基本信息。主要配置项如下:
spec_version: "2.1" # 配置规范版本
app_id: com.mycompany.myapp # 应用ID
version: 0.1.0 # 应用版本号
support_arch: # 支持的CPU架构
- amd64
- arm64
start_cmd: bin/myapp_serv --port=21010 # 应用后端服务启动命令
port: 21010 # 应用后端http服务端口
proxy_path: api # 应用后端http服务代理路径
open_type: inner # 应用前端页面打开方式
tag_types: # 应用类别
- utility
- devtool
depend_fw_version: 1.13.0.0000
i18n: # 应用详情页展示信息(多语言)
en-US:
name: My APP # 应用显示名称
description: My APP Desc # 应用描述
author: My Company # 应用开发者
official: https://myapp.example.com # 应用官网链接
help: https://myapp.example.com/help # 应用使用帮助网页链接
publisher: My Company # 应用发布者
publisher_link: https://mycompany.example.com # 应用发布者官网链接
zh-CN:
name: 演示应用
description: 演示应用描述
author: 演示应用开发者
official: https://myapp.example.com.cn
help: https://myapp.example.com.cn/help
publisher: 演示应用发布者
publisher_link: https://mycompany.example.com.cn拷贝应用文件
将应用后端服务可执行文件及前端文件拷贝到应用项目对应目录下。
- 将两种架构的后端服务可执行文件拷贝到对应的 rootfs 目录下, 如 amd64 架构的就拷贝到
rootfs_amd64/bin/myapp_serv - 将应用前端文件 www 目录下的文件都拷贝到
rootfs_common/www目录下。
制作应用图标
请参考应用图标制作规范,制作应用图标并替换 rootfs_common/www/icon.png 文件。(开发阶段可跳过此步骤,使用默认图标)
图标制作规范:
- 图标尺寸:256*256 像素
- 图标格式:PNG
- 文件大小:小于 100KB
- 图标背景:白色或者浅色背景加 0.5 粗的 8% 黑描边
- 图标形状:正方形,需要套用平滑圆角模板
打包应用
在应用项目根目录下执行 ugcli pack 命令打包应用,生成应用安装包 upk 文件。
命令示例:ugcli pack --build 1,其中 --build 参数为构建号,将和 project.yaml 中的版本号一起组成最终的应用版本号。例如 project.yaml 中版本号为 0.1.0,则最终版本号为 0.1.0.1。
最终生成的 upk 文件命名格式:{amd64/arm64}_{appid}_{version}.upk。