Skip to content

开发应用

应用开发主要包含以下步骤:

  • 开发并编译应用后端服务
  • 使用 web 前端技术开发应用前端界面
  • 使用开发者工具 ugcli 创建应用项目
  • 编写应用配置文件 project.yaml
  • 拷贝应用后端服务可执行文件及前端文件到应用项目对应目录下
  • 制作应用图标并拷贝到应用项目对应目录下
  • 使用开发者工具 ugcli 打包应用

下面我们将通过从零开始创建一个简单的示例应用,介绍如何快速开发应用并打包发布。你将快速学习应用开发的基本流程和开发者工具 ugcli 的基本使用。

开发后端服务

UGOS Pro 应用的后端服务目前仅支持使用 C/C++、Go 等编译型语言开发,需要编译生成可在 Linux 系统上直接运行的可执行文件。

本示例中,后端服务使用 Go 语言开发,实现一个简单的 HTTP 服务,提供心跳接口。

创建后端服务目录 backend,并编写 main.go 文件:

点击查看代码
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 的目录结构如下:

shell
backend/
├── go.mod
├── main.go
└── myapp_serv

开发前端页面

你可以自由选择前端框架,如 Vue、React 等,开发你的应用界面。完成开发后,需要将构建产物(HTML、CSS、JS 等)放入应用包中的 www 目录即可。

本示例应用的前端页面将使用 HTML + CSS + JavaScrip 开发, 实现一个简单的用户界面,用于展示后端服务的心跳状态。

创建 www/index.html 文件,内容如下:

点击查看代码
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 文件,内容如下:

点击查看代码
javascript
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 文件,内容如下:

点击查看代码
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 的目录结构如下:

shell
www
├── css
   └── style.css
├── index.html
└── js
    └── app.js

css/js 文件浏览器缓存处理

系统 web 服务器在响应 css/js 文件时会设置 Cache-Control 头允许浏览器缓存。 避免应用新版本发布后,用户浏览器仍然使用旧版本的文件,推荐采用如下其中一种方式处理:

  • 文件名哈希,如 app.a1b2c3d4.js,修改文件内容时同时修改文件名
  • 查询参数携带版本号,如 app.js?v=1.0.1,发布新版本时更新版本号

创建应用项目

运行 ugcli create com.mycompany.myapp 创建应用项目,完成创建后将会在当前目录下生成 com.mycompany.myapp 目录,结构如下:

shell
com.mycompany.myapp
├── project.yaml            # 应用配置文件
├── rootfs_amd64            # x86_64 架构的应用文件
   └── bin
├── rootfs_arm64            # arm64 架构的应用文件
   └── bin
├── rootfs_common           # 两个架构共用的应用文件
   ├── icon.png            # 应用图标
   └── www                 # 应用前端文件

编写应用配置文件

修改应用项目根目录下的 project.yaml 文件,配置应用的基本信息。主要配置项如下:

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