鸿蒙应用开发实战指南从零搭建开发环境到成功上架全流程详解华为官方文档解析
一、为什么选择鸿蒙?这不是一个选择题
2024年,当很多人还在纠结”要不要学鸿蒙”的时候,我遇到的第一个开发者小李告诉我:他花了一个月把App搬上鸿蒙,结果用户量直接翻了3倍。这听起来像故事,但华为官方的数据不会骗人——目前鸿蒙生态设备已经突破15亿台,而且这个数字还在以每月数百万的速度增长。
更重要的是,鸿蒙的声明周期比iOS短,比Android快,而且它有一套完全独立的应用商店审核机制。这意味着什么?意味着你现在入场,不是”补坑”,而是”占位”。
我们今天不谈情怀,不谈国产替代这些宏大叙事。你就当自己是个想多一条路的手艺人,我陪你把这个事情从头到尾捋清楚。
二、准备工作:先把地基打好
2.1 你需要什么?
别一上来就打开编译器,先看自己手里有什么牌:
| 条件 | 最低要求 | 推荐配置 |
|---|---|---|
| 操作系统 | Windows 10 64位 / macOS 11+ | Windows 11 或 macOS 12+ |
| 内存 | 8GB | 16GB起 |
| 硬盘 | 10GB可用空间 | 50GB SSD |
| 网络 | 稳定联网 | 能访问华为开发者官网 |
| 设备 | 无 | 一台华为/荣耀手机(可选,真机调试用) |
2.2 华为开发者账号:第一步,注册
打开浏览器,访问 developer.huawei.com,右上角点击”立即注册”。
这里有个很多人忽略的坑——账号类型选择:
- 个人账号:适合练手、提交个人应用,但上架应用市场时功能受限,推送、付费等功能拿不到
- 企业账号:正经做产品必须走这个,需要营业执照、开发者信息审核,审核时间大约3-5个工作日
建议一上来就注册企业账号。花半天准备材料,后面省一个月弯路。
注册流程其实不复杂,但有几个细节必须注意:
注册企业账号必做准备清单:
✅ 企业营业执照(需在有效期内)
✅ 企业对公银行账户信息
✅ 法人身份证正反面照片
✅ 开发者管理人脸部识别(需要法人配合)
✅ 企业资质认证资料(部分行业需要额外许可证明)
注册完成后,进入控制台 → 我的应用 → 创建应用。这里填写的应用名称不要带”测试”“demo”这种词,否则审核大概率被打回。
三、开发工具:DevEco Studio 安装全攻略
3.1 下载与安装
DevEco Studio是基于IntelliJ IDEA深度定制的开发环境,华为官方下载地址:
https://developer.huawei.com/consumer/cn/devecostudio/
支持Windows和macOS双平台,根据系统选择对应版本。下载完成后,双击安装程序,一路下一步就行。
但这里有个关键步骤——安装完成后首次启动,会自动下载SDK和工具链。这个过程取决于你的网速,慢的话可能要20分钟以上,建议提前连好Wi-Fi,别用流量。
3.2 配置镜像源(国内开发者的必修课)
如果你在国内,直接下载SDK可能会卡在99%。原因很简单——华为的服务器在海外,国内访问不稳定。
解决方式是在安装过程中配置华为的国内镜像:
打开 DevEco Studio → Settings → Appearance & Behavior → System Settings → Huawei Developer SDK
将默认的下载源地址替换为:
https://repo.huaweicloud.com/deveco-ide/
如果你在安装过程中就已经卡住了,可以先杀掉进程,通过命令行方式配置:
# macOS/Linux 用户可以在终端执行
export DEVECO_SDK_MIRROR=https://repo.huaweicloud.com/deveco-ide/
# Windows 用户在 PowerShell 中执行
$env:DEVECO_SDK_MIRROR="https://repo.huaweicloud.com/deveco-ide/"
3.3 创建你的第一个项目
启动 DevEco Studio,点击”Create Project”,看到这样的界面:
Project Type 选择:
├── Ability Template(传统多Ability架构,适合复杂应用)
├── Empty Ability(极简模板,适合学习)
└── Stage Model(推荐!鸿蒙4.0+主流开发模式)
Template 选择:
├── Empty(空白模板)
├── Hello World(带示例代码)
└── My Application(完整应用结构)
新手建议:选择 Stage Model + Empty,然后再自己搭建结构。这样你能看到每一行代码是怎么来的,而不是被模板牵着走。
填完项目名称、包名(格式必须为 com.你的公司/个人名.应用名)、保存路径后,点Finish。等待Gradle同步完成——这一步同样需要科学上网或者用前面说的镜像源。
四、鸿蒙开发基础概念:先搞懂再动手
很多教程一上来就讲代码,但鸿蒙有个核心概念你不理解,后面每一步都会卡住。
4.1 Stage模型 vs Ability模型
这是鸿蒙开发最大的分水岭。
Ability模型(HarmonyOS 3及以下):
┌─────────────────────────────────┐
│ UIAbility(负责UI界面) │
│ DataAbility(负责数据存储) │
│ BackgroundAbility(后台服务) │
└─────────────────────────────────┘
每个Ability独立生命周期,管理复杂
Stage模型(HarmonyOS 4+推荐):
┌─────────────────────────────────┐
│ FA(Feature Ability) │
│ ├─ 页面层(Page Ability) │
│ └─ 服务层(Service Ability) │
└─────────────────────────────────┘
生命周期统一,与React/Vue的组件化思想更接近
如果你之前有过Web开发经验,Stage模型会让你感觉非常亲切——它本质上就是”组件化+声明式UI”,只不过渲染的是原生控件而不是HTML。
4.2 ArkTS语言:TypeScript的升级版
鸿蒙开发用的语言叫ArkTS,它不是TypeScript,但语法几乎和TS一模一样。
// 这是一个最简单的鸿蒙组件示例
@Component
struct HelloWorld {
private count: number = 0
build() {
Column() {
Text('你好,鸿蒙')
.fontSize(24)
.fontColor(Color.Black)
Button('点击次数:' + this.count)
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
看到没有?和Vue的<template> + <script>几乎一个逻辑。唯一的区别是:这里的build()方法返回的是容器声明式UI的树形结构,而不是HTML字符串。
4.3 声明式UI的核心思想
传统Android开发喜欢用findViewById然后setText,这种方式叫命令式UI——你手动告诉系统”这里改成什么”。
鸿蒙用的是声明式UI——你描述”这里应该长什么样”,系统自动帮你渲染。
命令式UI(Android Java):
TextView tv = findViewById(R.id.tv);
tv.setText("你好");
声明式UI(鸿蒙ArkTS):
Text("你好") // 系统自己知道什么时候渲染
这个思维转变不做好,看官方文档会觉得”这写的是什么”。
五、从零写一个完整应用
光概念没用,我们直接动手。今天我们要做一个”每日签到”应用——功能简单,但能覆盖鸿蒙开发的核心知识点:页面路由、状态管理、网络请求、本地存储。
5.1 项目结构设计
signdaily/
├── entry/src/main/
│ ├── resources/
│ │ └── base/
│ │ └── element/
│ │ └── string.json // 字符串资源
│ ├── module.json5 // 模块配置文件
│ └── ets/
│ ├── entryability/
│ │ └── EntryAbility.ets // 应用入口
│ ├── pages/
│ │ ├── Index.ets // 首页(签到页)
│ │ ├── Record.ets // 记录页
│ │ └── Profile.ets // 个人中心
│ ├── components/
│ │ ├── CalendarView.ets // 自定义日历组件
│ │ └── SignButton.ets // 签到按钮组件
│ ├── viewmodel/
│ │ └── SignViewModel.ets // 状态管理
│ ├── utils/
│ │ ├── storage.ts // 本地存储工具
│ │ └── request.ts // 网络请求封装
│ └── api/
│ └── signApi.ts // API接口定义
└── build-profile.json5 // 构建配置
5.2 应用入口配置
// entry/src/main/ets/entryability/EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility'
import hilog from '@ohos.hilog'
import window from '@ohos.window'
export default class EntryAbility extends UIAbility {
// 应用创建时调用
onCreate(want, launchParam) {
hilog.info(0x0000, 'EntryAbility', 'onCreate start')
}
// 应用界面显示时调用
onWindowStageCreate(windowStage: window.WindowStage) {
// 加载主页面
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'EntryAbility', 'Failed to load content')
return
}
hilog.info(0x0000, 'EntryAbility', 'Succeeded in loading content')
})
}
onForeground() {
hilog.info(0x0000, 'EntryAbility', 'OnForeground')
}
}
5.3 首页开发——签到功能
这是整个应用最核心的部分:
// entry/src/main/ets/pages/Index.ets
import { SignViewModel } from '../viewmodel/SignViewModel'
import { signApi } from '../api/signApi'
// 引入自定义组件
import CalendarView from '../components/CalendarView'
import SignButton from '../components/SignButton'
// 状态管理
@State viewModel: SignViewModel = new SignViewModel()
@Component
struct Index {
// 页面生命周期钩子
onPageShow() {
this.viewModel.refreshSignStatus()
}
build() {
NavDestination() {
Column() {
// 顶部导航栏
this.BuildHeader()
// 日历组件
CalendarView({
currentYear: this.viewModel.currentYear,
currentMonth: this.viewModel.currentMonth,
signDates: this.viewModel.signDates
})
// 签到统计
this.BuildStats()
// 签到按钮
SignButton({
isSigned: this.viewModel.isSignedToday,
onSign: () => this.handleSign()
})
// 空状态提示
if (this.viewModel.isLoading) {
this.BuildLoading()
}
}
.width('100%')
.height('100%')
.padding({ top: 20, bottom: 20 })
}
}
// 顶部导航
BuildHeader() {
Row() {
Text('每日签到')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
Blank()
Text('今天 ' + this.viewModel.getWeekday())
.fontSize(14)
.fontColor('#888888')
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
}
// 签到统计
BuildStats() {
Row() {
this.BuildStatItem('连续签到', this.viewModel.streakDays.toString() + '天', '#FF6B6B')
this.BuildStatItem('累计签到', this.viewModel.totalDays.toString() + '天', '#4ECDC4')
this.BuildStatItem('本月签到', this.viewModel.monthDays.toString() + '天', '#45B7D1')
}
.width('100%')
.justifyContent(SpaceBetween)
.padding({ left: 24, right: 24, top: 16, bottom: 16 })
}
BuildStatItem(label: string, value: string, color: string) {
Column() {
Text(value)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(color)
Text(label)
.fontSize(12)
.fontColor('#888888')
.margin({ top: 4 })
}
}
// 加载状态
BuildLoading() {
Column() {
LoadingProgress()
.width(40)
.height(40)
.color('#007DFF')
Text('加载中...')
.fontSize(14)
.fontColor('#888888')
.margin({ top: 12 })
}
.width('100%')
.padding(20)
}
// 处理签到
async handleSign() {
if (this.viewModel.isSignedToday) {
// 已签到,显示提示
this.showToast('今天已经签到啦!')
return
}
this.viewModel.setLoading(true)
try {
const result = await signApi.sign()
if (result.success) {
this.viewModel.refreshSignStatus()
this.showToast('签到成功!+10积分')
// 播放签到成功动画
this.playSuccessAnimation()
} else {
this.showToast(result.message || '签到失败,请重试')
}
} catch (e) {
this.showToast('网络异常,请检查网络后重试')
hilog.error(0x0000, 'Index', 'Sign failed: ' + JSON.stringify(e))
} finally {
this.viewModel.setLoading(false)
}
}
showToast(msg: string) {
// 使用鸿蒙原生Toast
ToastUtils.showToast(msg)
}
playSuccessAnimation() {
// 播放庆祝动画
// 这里可以调用资源中的动画
}
}
5.4 状态管理——ViewModel
// entry/src/main/ets/viewmodel/SignViewModel.ets
import { SignRecord, SignInRecord } from '../utils/storage'
import { formatDate, getMonthDays, getDaysInMonth } from '../utils/dateUtils'
// 使用观察者模式管理状态
export class SignViewModel {
// 当前日期
currentDate: Date = new Date()
currentYear: number = this.currentDate.getFullYear()
currentMonth: number = this.currentDate.getMonth() + 1
// 签到状态
isSignedToday: boolean = false
streakDays: number = 0
totalDays: number = 0
monthDays: number = 0
// 签到日期记录
signDates: string[] = []
// 加载状态
isLoading: boolean = false
// 获取今日格式化的日期字符串
getTodayStr(): string {
return formatDate(this.currentDate)
}
// 刷新签到状态
async refreshSignStatus() {
const today = this.getTodayStr()
// 从本地存储读取数据
const records = await SignInRecord.getAll()
this.signDates = records.map(r => r.date)
// 计算连续签到天数
this.streakDays = this.calculateStreak()
// 计算累计签到天数
this.totalDays = this.signDates.length
// 计算本月签到天数
this.monthDays = this.signDates.filter(d => {
const date = new Date(d)
return date.getFullYear() === this.currentYear &&
date.getMonth() + 1 === this.currentMonth
}).length
// 检查今天是否已签到
this.isSignedToday = this.signDates.includes(today)
}
// 计算连续签到天数
calculateStreak(): number {
if (this.signDates.length === 0) return 0
const sortedDates = [...this.signDates].sort((a, b) =>
new Date(b).getTime() - new Date(a).getTime()
)
const today = this.getTodayStr()
let streak = 0
let checkDate = new Date(today)
for (let i = 0; i < sortedDates.length; i++) {
const formattedDate = formatDate(checkDate)
if (sortedDates[i] === formattedDate) {
streak++
checkDate.setDate(checkDate.getDate() - 1)
} else {
break
}
}
return streak
}
// 获取星期几
getWeekday(): string {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return weekdays[this.currentDate.getDay()]
}
// 设置加载状态
setLoading(state: boolean) {
this.isLoading = state
}
}
5.5 网络请求封装
// entry/src/main/ets/utils/request.ets
import http from '@ohos.net.http'
// 请求配置
interface RequestConfig {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
headers?: Record<string, string>
data?: object
timeout?: number
}
// 响应类型
interface Response<T> {
success: boolean
data: T
message: string
code: number
}
// 统一的请求封装
export class HttpClient {
private baseUrl: string = 'https://api.example.com'
private timeout: number = 10000
// 发起GET请求
async get<T>(url: string, params?: Record<string, string>): Promise<Response<T>> {
const request = http.createHttp()
try {
const fullUrl = params
? `${this.baseUrl}${url}?${this.buildQuery(params)}`
: `${this.baseUrl}${url}`
const response = await request.request(fullUrl, {
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getToken()}`
},
connectTimeout: this.timeout,
readTimeout: this.timeout
}) as http.HttpResponse
return this.parseResponse<T>(response)
} finally {
request.destroy()
}
}
// 发起POST请求
async post<T>(url: string, data?: object): Promise<Response<T>> {
const request = http.createHttp()
try {
const response = await request.request(`${this.baseUrl}${url}`, {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getToken()}`
},
extraData: data,
connectTimeout: this.timeout,
readTimeout: this.timeout
}) as http.HttpResponse
return this.parseResponse<T>(response)
} finally {
request.destroy()
}
}
// 解析响应
private parseResponse<T>(response: http.HttpResponse): Response<T> {
const result = JSON.parse(response.result as string)
return {
success: response.responseCode === 200 && result.code === 0,
data: result.data as T,
message: result.message || '操作成功',
code: result.code
}
}
// 构建查询字符串
private buildQuery(params: Record<string, string>): string {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
}
// 获取Token
private getToken(): string {
// 这里应该从安全存储中读取
return ''
}
}
// 单例
export const httpClient = new HttpClient()
5.6 本地存储工具
// entry/src/main/ets/utils/storage.ets
import preferences from '@ohos.data.preferences'
import hilog from '@ohos.hilog'
// 签到记录类型
export interface SignRecord {
date: string
points: number
timestamp: number
}
export class SignInRecord {
private static PREFERENCES_NAME = 'sign_record_pref'
private static KEY_SIGN_DATES = 'sign_dates'
private static KEY_TOTAL_POINTS = 'total_points'
private static KEY_STREAK_DAYS = 'streak_days'
// 保存签到记录
static async saveRecord(record: SignRecord): Promise<void> {
try {
const pref = await preferences.getPreferences(this.PREFERENCES_NAME)
// 读取已有记录
const existingData = pref.get(this.KEY_SIGN_DATES, '[]') as string
const records: SignRecord[] = JSON.parse(existingData)
// 追加新记录
records.push(record)
// 更新数据
await pref.put(this.KEY_SIGN_DATES, JSON.stringify(records))
await pref.flush()
hilog.info(0x0000, 'SignInRecord', 'Record saved successfully')
} catch (e) {
hilog.error(0x0000, 'SignInRecord', 'Failed to save record: ' + JSON.stringify(e))
}
}
// 获取所有签到记录
static async getAll(): Promise<SignRecord[]> {
try {
const pref = await preferences.getPreferences(this.PREFERENCES_NAME)
const data = pref.get(this.KEY_SIGN_DATES, '[]') as string
return JSON.parse(data)
} catch (e) {
hilog.error(0x0000, 'SignInRecord', 'Failed to get records: ' + JSON.stringify(e))
return []
}
}
// 获取累计积分
static async getTotalPoints(): Promise<number> {
try {
const pref = await preferences.getPreferences(this.PREFERENCES_NAME)
return pref.get(this.KEY_TOTAL_POINTS, 0) as number
} catch (e) {
return 0
}
}
// 更新累计积分
static async updateTotalPoints(points: number): Promise<void> {
try {
const pref = await preferences.getPreferences(this.PREFERENCES_NAME)
await pref.put(this.KEY_TOTAL_POINTS, points)
await pref.flush()
} catch (e) {
hilog.error(0x0000, 'SignInRecord', 'Failed to update points: ' + JSON.stringify(e))
}
}
}
六、UI组件开发:日历和签到按钮
6.1 日历组件
// entry/src/main/ets/components/CalendarView.ets
@Component
struct CalendarView {
@Prop currentYear: number = 2024
@Prop currentMonth: number = 1
@Prop signDates: string[] = []
// 每月天数
private daysInMonth: number = 31
// 每月第一天是周几
private firstDayOfWeek: number = 0
// 已选中的日期
@State selectedDate: string = ''
aboutToAppear() {
this.calculateMonthDays()
this.selectedDate = this.formatDate(new Date())
}
calculateMonthDays() {
const date = new Date(this.currentYear, this.currentMonth - 1, 1)
this.daysInMonth = new Date(this.currentYear, this.currentMonth, 0).getDate()
this.firstDayOfWeek = date.getDay()
}
// 格式化日期为字符串
formatDate(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 判断日期是否已签到
isSigned(dateStr: string): boolean {
return this.signDates.includes(dateStr)
}
build() {
Column() {
// 月份标题
Row() {
Text(`${this.currentYear}年${this.currentMonth}月`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
Blank()
Text('已签到')
.fontSize(12)
.fontColor('#007DFF')
.backgroundColor('#E8F4FF')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(12)
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 12 })
// 星期标题
Row() {
ForEach(['日', '一', '二', '三', '四', '五', '六'], (day: string) => {
Text(day)
.fontSize(14)
.fontColor('#888888')
.width(44)
.textAlign(TextAlign.Center)
})
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 8 })
// 日期网格
Grid() {
// 空白天数
ForEach(Array(this.firstDayOfWeek).fill(null), () => {
GridItem()
})
// 日期
ForEach(Array(this.daysInMonth).fill(null).map((_, i) => i + 1), (day: number) => {
GridItem() {
this.buildDayCell(day)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
.columnsGap(0)
.rowsGap(0)
.width('100%')
.padding({ left: 16, right: 16 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
.padding(16)
.margin({ left: 16, right: 16, bottom: 16 })
}
// 日期格子
buildDayCell(day: number) {
const dateStr = `${this.currentYear}-${String(this.currentMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`
const isToday = dateStr === this.formatDate(new Date())
const isSigned = this.isSigned(dateStr)
Column() {
Text(String(day))
.fontSize(14)
.fontWeight(isToday ? FontWeight.Bold : FontWeight.Normal)
.fontColor(isSigned ? '#FFFFFF' : (isToday ? '#007DFF' : '#1A1A1A'))
.width(40)
.height(40)
.textAlign(TextAlign.Center)
.verticalAlign(VerticalAlign.Middle)
.backgroundColor(isSigned ? '#007DFF' : 'transparent')
.borderRadius(20)
}
.width('100%')
.height(48)
}
}
6.2 签到按钮组件
// entry/src/main/ets/components/SignButton.ets
@Component
struct SignButton {
@Prop isSigned: boolean = false
onSign: () => void = () => {}
// 按钮动画状态
@State isAnimating: boolean = false
build() {
Column() {
if (this.isSigned) {
// 已签到状态
Row() {
Image($r('app.media.sign_success'))
.width(48)
.height(48)
Column() {
Text('今日已签到')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
Text('明天继续加油!')
.fontSize(14)
.fontColor('#888888')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
}
.width('100%')
.padding(20)
.backgroundColor('#F1F8E9')
.borderRadius(12)
} else {
// 未签到状态 - 大按钮
Button('立即签到')
.width('90%')
.height(56)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.backgroundColor('#007DFF')
.borderRadius(28)
.onClick(() => {
this.isAnimating = true
this.onSign()
setTimeout(() => {
this.isAnimating = false
}, 1000)
})
.animation({
duration: this.isAnimating ? 300 : 0,
iterations: this.isAnimating ? 1 : 0
})
}
// 签到提示
Text('签到可获得10积分,连续签到还有额外奖励!')
.fontSize(12)
.fontColor('#888888')
.textAlign(TextAlign.Center)
.margin({ top: 12 })
}
.width('100%')
}
}
七、应用配置:module.json5详解
每个鸿蒙应用都有一个模块配置文件,它告诉系统你的应用长什么样、需要什么权限、有哪些页面。
// entry/src/main/module.json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "每日签到应用",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet"], // 支持的手机和平板
"deliveryWithInstall": true,
"installationFree": false,
"pages": [
"profiles/main_profile"
],
// 应用入口Ability
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "应用主页面",
"icon": "$media:app_icon",
"label": "每日签到",
"startWindowIcon": "$media:splash_icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
// 启动模式
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
],
// 网络权限
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"usedScene": {
"abilities": ["EntryAbility"],
"desc": "需要网络权限来请求签到数据"
}
}
],
// 应用图标和主题
"metadata": [
{
"name": "appIcon",
"value": "$media:app_icon"
},
{
"name": "appLaunchBackgroundColor",
"value": "#FFFFFF"
}
]
}
}
这里最容易被忽略的是deviceTypes——如果你只写"phone",那平板用户就看不到你的应用了。鸿蒙强调”一次开发,多端部署”,所以建议一开始就声明支持所有设备类型。
八、真机调试:从模拟器到真机
8.1 使用模拟器快速测试
DevEco Studio内置了模拟器,适合早期开发阶段:
菜单栏 → Tools → Device Manager → Create Emulator
推荐选择:
- 设备类型:Phone
- 系统版本:HarmonyOS 4.0+
- 分辨率:1080 x 2340(主流手机分辨率)
模拟器启动后,点击工具栏的绿色三角形▶️按钮即可运行应用。这是最快的验证方式,不需要连任何设备。
8.2 真机调试配置
真机调试能发现模拟器发现不了的问题(比如性能、传感器、网络等)。
步骤:
开启开发者模式
- 手机设置 → 关于手机 → 版本号,连续点击7次
- 设置 → 系统和更新 → 开发者选项 → 开启USB调试
连接电脑
- 用USB线连接手机和电脑
- 手机上弹出”允许USB调试”时,点击允许
在DevEco Studio中选择设备
- 顶部工具栏会显示已连接的设备
- 如果没有显示,点击Refresh刷新
配置签名
- 真机调试需要签名,首次运行会引导你创建调试证书
- 按提示完成即可,调试证书只在开发环境有效
注意:调试证书不能用于上架,上架需要单独的发布证书
九、打包与签名:上架前的最后一步
9.1 配置发布签名
- 打开 DevEco Studio → Build → Distribute APP → Hap/APP Packager
- 选择”Create new signature”
- 填写证书信息:
- 证书名称:自定义(如”SignDaily_Release”)
- 密钥库密码:记住这个密码
- 密钥密码:记住这个密码
- 有效期:建议5年以上
- 公司信息:需要与实际企业账号一致
9.2 生成打包文件
Build → Build Hap(s)/APP(s) → Build Hap(s)
生成后会得到两种格式:
.hap:单个模块包,适合内部测试.app:完整安装包,适合上架
9.3 本地测试验证
在提交之前,建议先在内部跑一遍验证:
// 在Index.ets中添加测试入口
@Entry
@Component
struct TestIndex {
build() {
Column() {
Button('测试完整流程')
.onClick(() => {
// 模拟完整的签到流程
this.testSignFlow()
})
}
}
async testSignFlow() {
// 1. 检查网络
const network = await this.checkNetwork()
if (!network) {
console.error('无网络连接')
return
}
// 2. 获取用户信息
const userInfo = await this.getUserInfo()
// 3. 发起签到
const result = await signApi.sign()
// 4. 验证结果
if (result.success) {
// 5. 更新本地数据
await SignInRecord.saveRecord({
date: this.getTodayStr(),
points: 10,
timestamp: Date.now()
})
console.info('完整流程测试通过')
}
}
}
十、应用上架全流程
10.1 准备工作清单
在点击”提交审核”之前,确认以下事项全部完成:
上架前检查清单:
□ 应用名称不与已有应用重复
□ 应用图标符合规范(1024x1024 PNG,无圆角)
□ 应用截屏已准备(至少3张,建议5张)
□ 应用描述文案已完成
□ 隐私政策链接已配置
□ 应用权限申请合理(不过度申请)
□ 测试包已在内部跑通
□ 关键功能已回归测试
□ 已知问题已修复
10.2 创建应用信息
登录 AppGallery Connect → 我的应用 → 创建应用
填写内容:
| 字段 | 要求 | 建议 |
|---|---|---|
| 应用名称 | 2-30个字符 | 简洁好记,不要带标点 |
| 应用包名 | 必须唯一 | 格式:com.company.appname |
| 应用分类 | 选最贴近的 | 工具类、效率类、生活类等 |
| 应用描述 | 最多100字 | 突出核心价值,不要太长 |
| 详细介绍 | 最多5000字 | 详细说明功能和使用方法 |
| 客服邮箱 | 必须有效 | 用于审核沟通 |
| 客服电话 | 可选 | 建议填写 |
| 官网链接 | 可选 | 有就填 |
10.3 上传应用包
- 进入应用 → 版本管理 → 创建版本
- 上传打包好的
.app文件 - 填写版本信息:
- 版本号:建议用
主版本.次版本.修订号格式,如1.0.0 - 更新日志:说明本次更新内容
- 版本号:建议用
- 配置内测/公测(可选):可以先小范围测试再正式上架
10.4 隐私合规配置(关键!)
这是很多人卡住的地方。华为对隐私合规审查非常严格:
必须在应用内配置:
1. 隐私政策弹窗
- 首次安装打开应用时,必须弹出隐私政策
- 用户同意后才能使用应用功能
- 必须在设置中提供查看隐私政策的入口
2. 权限申请说明
- 每个权限申请都要有明确的使用场景说明
- 不能无缘无故申请敏感权限(如通讯录、短信、位置等)
3. 数据收集声明
- 明确告知用户收集了哪些数据
- 告知数据用途
- 提供用户删除数据的入口
代码示例:
// 隐私政策弹窗组件
@Component
struct PrivacyDialog {
@State showPrivacy: boolean = false
aboutToAppear() {
// 检查用户是否已同意隐私政策
preferences.getPreferences('privacy_pref')
.then(pref => {
const agreed = pref.get('privacy_agreed', false)
this.showPrivacy = !agreed
})
}
build() {
if (this.showPrivacy) {
Stack() {
// 遮罩层
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
// 弹窗内容
Column() {
Text('隐私政策')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
Text('我们非常重视您的个人信息和隐私保护...')
.fontSize(14)
.fontColor('#333333')
.textAlign(TextAlign.Start)
.maxLines(6)
.overflow(TextOverflow.Ellipsis)
.margin({ bottom: 24 })
Row() {
Button('拒绝')
.fontSize(16)
.fontColor('#888888')
.backgroundColor(Color.Transparent)
.width('45%')
.height(48)
.onClick(() => {
// 拒绝则退出应用
process.exit()
})
Button('同意')
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor('#007DFF')
.width('45%')
.height(48)
.borderRadius(24)
.onClick(() => {
// 保存同意状态
preferences.put('privacy_pref', 'privacy_agreed', true)
this.showPrivacy = false
})
}
.width('100%')
.justifyContent(SpaceBetween)
}
.width('85%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
.padding(24)
}
.width('100%')
.height('100%')
}
}
}
10.5 提交审核
填写完整信息后,点击”提交审核”。审核时间通常为:
普通应用:3-7个工作日
金融/社交类应用:7-15个工作日
游戏类应用:7-15个工作日
审核期间保持手机畅通,审核人员可能会通过客服邮箱联系你。
10.6 常见问题与解决方案
❌ 审核被拒:隐私政策不完善
解决:检查是否所有敏感操作都有对应的隐私说明
❌ 审核被拒:应用名称与已有应用相似
解决:修改应用名称,避免与知名应用混淆
❌ 审核被拒:应用图标不规范
解决:确保图标是纯PNG,尺寸正确,无圆角(系统会自动加圆角)
❌ 审核被拒:过度申请权限
解决:只申请应用必需的权限,并在module.json5中说明每个权限的用途
❌ 上架后闪退
解决:使用华为的崩溃采集服务(HMS Core Crash Service)定位问题
十一、上架后的运营与维护
11.1 数据分析
上架不是终点。华为提供完善的数据分析平台:
AppGallery Connect → 数据分析 → 应用分析
可以查看:
- 日活跃用户(DAU)
- 新增用户趋势
- 用户留存率
- 页面访问路径
- 崩溃统计
11.2 版本更新流程
发现bug或需要新功能时:
- 修改代码并测试
- 升级版本号(如从
1.0.0升级到1.0.1) - 重新打包签名
- 在AppGallery Connect创建新版本
- 提交审核(更新审核通常1-3个工作日)
11.3 用户反馈处理
保持以下渠道畅通:
□ 应用内的客服邮箱
□ AppGallery的开发者后台消息
□ 用户评论回复
快速响应用户反馈可以显著提升应用评分
十二、给新手的几点真心话
写到这里,我想说几句实在的。
第一,不要试图一次做完所有功能。 我见过太多人上来就想做一个”大而全”的应用,结果半年过去了连第一个版本都出不来。先做一个最小可用版本(MVP),上架,拿到用户反馈,再迭代。
第二,文档比教程重要。 官方文档 developer.huawei.com/consumer/cn/doc/harmonyos-guides 是最权威的资料,遇到问题先去文档里搜,90%的问题都能找到答案。
第三,加入开发者社区。 华为开发者论坛 有很多实战经验,遇到问题发帖,经常能得到官方工程师的回复。
第四,重视隐私合规。 这不是形式,是真正的法律要求。《个人信息保护法》对App的合规要求越来越严,华为的审核也是在这个大背景下加强的。
第五,保持耐心。 鸿蒙生态还在快速发展期,现在入场不晚。但开发过程会遇到各种奇怪的报错、审核被打回、用户反馈bug——这些都是正常的。把每一个问题当成学习的机会,你会成长得很快。
开发鸿蒙应用这件事,说难也难,说不难也不难。难的是你第一次打开DevEco Studio时面对的那些新概念,不难的是当你理解了声明式UI的思想之后,你会发现它比传统的命令式开发更高效、更优雅。
希望这篇文章能帮你跨过第一道门槛。如果过程中遇到问题,记得——先查文档,再问社区,最后再找我。加油!