【江鸟中原】鸿蒙作业:类似QQ的聊天软件
本文基于鸿蒙ArkTS的entry工程,完整实现了模拟QQ App的核心功能,重点拆解了登录体系的开发与坑点解决方案,同时覆盖了消息列表、个人中心等核心场景。开发过程中需重点关注「鸿蒙网络安全规范、ArkTS标准库限制、登录状态持久化」三大核心点,既能保证功能正常运行,也能符合鸿蒙应用的开发规范。该项目的架构设计与代码实现可直接复用至各类鸿蒙社交类App开发,开发者可基于此扩展更多QQ特色功能(如
鸿蒙ArkTS实战:模拟聊天软件开发(核心功能+登录体系全解析)
前言
QQ作为国民级社交应用,其核心体验涵盖登录认证、消息列表、联系人管理、个人中心等模块。基于鸿蒙ArkTS开发模拟聊天软件,既能深入掌握鸿蒙应用开发核心技术,也能理解大型社交软件的底层设计逻辑。本文以此为基础,从工程架构、登录体系、核心功能实现三个维度,完整讲解模拟聊天软件的开发过程,重点拆解登录模块的坑点与解决方案,提供可直接复用的代码与架构设计思路。
一、项目工程架构
1. 工程目录结构
模块遵循鸿蒙官方推荐的分层架构,核心目录如下:
entry
├── src/main/ets
│ ├── abilities // 应用入口
│ │ └── EntryAbility.ets // 全局生命周期、状态初始化
│ ├── common // 通用资源
│ │ ├── constants // 常量定义(接口地址、路由路径)
│ │ │ └── Config.ets
│ │ ├── styles // 全局样式(颜色、字体、间距)
│ │ │ └── CommonStyles.ets
│ │ └── utils // 工具类(网络、存储、加密、路由)
│ │ ├── RequestUtil.ets // 网络请求封装
│ │ ├── StorageUtil.ets // 本地存储(内存+持久化)
│ │ ├── EncryptUtil.ets // 密码加密
│ │ └── RouterUtil.ets // 路由封装
│ ├── models // 数据模型(接口返回、本地实体)
│ │ ├── UserModel.ets // 用户信息模型
│ │ ├── MessageModel.ets // 消息模型
│ │ └── LoginModel.ets // 登录相关模型
│ ├── pages // 页面(核心业务)
│ │ ├── login // 登录模块
│ │ │ ├── LoginPage.ets // QQ/手机号登录页
│ │ │ └── RegisterPage.ets // 注册页
│ │ ├── home // 首页模块
│ │ │ ├── MessagePage.ets // 消息列表页
│ │ │ ├── ContactPage.ets // 联系人页
│ │ │ └── MinePage.ets // 个人中心页
│ │ └── common // 通用页面
│ │ ├── GuidePage.ets // 新用户引导页
│ │ └── SettingPage.ets // 设置页
│ └── main_pages.json // 路由注册
└── src/main/resources
├── rawfile // 静态资源
│ └── network_security_config.json // 网络安全配置
└── media // 图片资源(QQ图标、默认头像等)
2. 核心技术选型
|
模块 |
技术/工具 |
应用场景 |
|
开发语言 |
ArkTS + TypeScript |
全量业务代码,类型安全保障 |
|
状态管理 |
AppStorage + Preferences |
登录状态(内存+持久化)、全局配置 |
|
网络通信 |
@ohos.net.http + 拦截器 |
登录请求、消息拉取、联系人同步 |
|
路由管理 |
@ohos.router + 路由封装 |
页面跳转、参数传递、路由栈管理 |
|
数据解析 |
JSON + 类型接口 |
前后端数据交互,避免any类型 |
|
安全加密 |
@ohos.crypto(MD5/SHA256) |
密码传输、Token加密存储 |
二、核心功能实现
1. 登录体系:QQ账号/手机号双登录(重点)
登录是模拟应用的入口,需实现「身份校验、状态持久化、安全防护」三大核心目标,解决鸿蒙开发中常见的网络、存储、标准库限制问题。
(1)网络安全配置(解决HTTP明文限制)
鸿蒙默认禁止HTTP明文传输,在rawfile/network_security_config.json配置允许后端接口域名:
{
"domainSettings": {
"cleartextPermitted": false,
"domains": [
{
"domain": "10.133.48.157", // 后端接口IP
"subdomains": true,
"cleartextPermitted": true
},
{
"domain": "10.0.2.2", // 模拟器测试IP
"subdomains": true,
"cleartextPermitted": true
}
]
}
}
在module.json5中引用配置并声明网络权限:
{
"module": {
"deviceConfig": {
"default": {
"network": {
"securityConfig": {
"domainSettings": "$rawfile:network_security_config.json"
}
}
}
},
"reqPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "QQ登录、消息拉取需要网络连接",
"usedScene": { "abilities": ["*"], "when": "always" }
}
]
}
}
(2)登录请求封装(兼容ArkTS标准库)
核心工具类utils/RequestUtil.ets,解决「标准库限制、Token自动携带、错误统一处理」问题:
import http from '@ohos.net.http';
import prompt from '@ohos.promptAction';
import AppStorage from '@ohos.app.ability.AppStorage';
import router from '@ohos.router';
import { Config } from '../constants/Config';
export class RequestUtil {
// 基础配置
private static BASE_URL = Config.BASE_API;
private static RETRY_COUNT = 1; // 重试次数
private static TIMEOUT = 10000; // 超时时间
// 参数序列化(过滤undefined,避免JSON解析错误)
private static serializeParams(params: any): string {
const validParams: Record<string, any> = {};
for (const key in params) {
if (params.hasOwnProperty(key) && params[key] !== undefined && params[key] !== null) {
validParams[key] = params[key];
}
}
return JSON.stringify(validParams);
}
// 统一请求方法
private static async request<T>(url: string, options: http.HttpRequestOptions): Promise<T> {
const fullUrl = `${this.BASE_URL}${url}`;
let request: http.HttpRequest | null = null;
let retryCount = 0;
while (retryCount <= this.RETRY_COUNT) {
try {
request = http.createHttp();
request.setTimeout({ timeout: this.TIMEOUT });
// 自动携带Token(登录后全局存储)
const token = AppStorage.get<string>('token') || '';
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
...options.header
};
const response = await request.request(fullUrl, {
...options,
header: headers,
usingCache: false
});
// 状态码处理
if (response.responseCode >= 200 && response.responseCode < 300) {
const resultStr = response.result as string;
if (!resultStr) throw new Error('接口返回空数据');
const data = JSON.parse(resultStr) as T;
return data;
} else if (response.responseCode === 401) {
// Token过期:清除状态并跳回登录页
prompt.showToast({ message: '登录已过期,请重新登录' });
AppStorage.delete('token');
AppStorage.delete('userInfo');
await router.replaceUrl({ url: Config.ROUTE_LOGIN });
throw new Error('Token失效');
} else {
throw new Error(`服务器错误:${response.responseCode}`);
}
} catch (error) {
retryCount++;
if (retryCount > this.RETRY_COUNT) {
const errMsg = error instanceof Error ? error.message : '网络请求失败';
if (errMsg.includes('ERR_CLEARTEXT_NOT_PERMITTED')) {
prompt.showToast({ message: '网络安全配置错误,请检查' });
} else {
prompt.showToast({ message: errMsg });
}
throw error;
}
// 重试延迟
await new Promise(resolve => setTimeout(resolve, 1000));
} finally {
request?.destroy();
}
}
throw new Error('请求失败');
}
// QQ账号登录
static async qqLogin(qqNumber: string, password: string) {
// 密码MD5加密(避免明文传输)
const encryptPwd = EncryptUtil.md5(password);
return this.request<LoginResponse>('/auth/qqLogin', {
method: http.RequestMethod.POST,
extraData: this.serializeParams({ qqNumber, password: encryptPwd })
});
}
// 手机号验证码登录
static async phoneLogin(phoneNumber: string, code: string) {
return this.request<LoginResponse>('/auth/phoneLogin', {
method: http.RequestMethod.POST,
extraData: this.serializeParams({ phoneNumber, code })
});
}
// 发送验证码
static async sendCode(phoneNumber: string) {
return this.request<BaseResponse>('/auth/sendCode', {
method: http.RequestMethod.POST,
extraData: this.serializeParams({ phoneNumber })
});
}
}
// 基础类型定义
interface BaseResponse {
success: boolean;
message: string;
}
interface LoginResponse extends BaseResponse {
data: {
token: string;
user: {
id: number;
qqNumber: string;
phoneNumber: string;
nickname: string;
avatar: string;
isNewUser: boolean;
};
};
}
(3)登录页面实现(LoginPage.ets)
兼顾「账号登录、手机号登录切换、加载状态、输入校验」,符合交互逻辑:
import router from '@ohos.router';
import prompt from '@ohos.promptAction';
import AppStorage from '@ohos.app.ability.AppStorage';
import { RequestUtil } from '../../common/utils/RequestUtil';
import { StorageUtil } from '../../common/utils/StorageUtil';
import { Config } from '../../common/constants/Config';
import { CommonStyles } from '../../common/styles/CommonStyles';
@Component
export struct LoginPage {
// 登录类型:qq/phone
@State loginType: string = 'qq';
// QQ登录字段
@State qqNumber: string = '';
@State qqPwd: string = '';
// 手机号登录字段
@State phoneNumber: string = '';
@State verifyCode: string = '';
@State countDown: number = 0;
@State isLoading: boolean = false;
build() {
Column() {
// QQ Logo
Image($r('app.media.qq_logo'))
.width(100)
.height(100)
.margin({ top: 80, bottom: 40 });
// 登录类型切换
Row() {
Text('QQ账号登录')
.fontSize(18)
.fontWeight(this.loginType === 'qq' ? FontWeight.Bold : FontWeight.Normal)
.color(this.loginType === 'qq' ? '#007AFF' : '#333')
.onClick(() => this.loginType = 'qq')
.padding(10);
Text('手机号登录')
.fontSize(18)
.fontWeight(this.loginType === 'phone' ? FontWeight.Bold : FontWeight.Normal)
.color(this.loginType === 'phone' ? '#007AFF' : '#333')
.onClick(() => this.loginType = 'phone')
.padding(10);
}
.margin({ bottom: 30 });
// 登录表单
if (this.loginType === 'qq') {
// QQ账号登录表单
this.renderQQForm();
} else {
// 手机号登录表单
this.renderPhoneForm();
}
// 登录按钮
Button('登录')
.style(CommonStyles.mainButton)
.disabled(this.isLoading || !this.checkFormValid())
.onClick(() => this.handleLogin())
.margin({ top: 30 });
// 加载状态
if (this.isLoading) {
Progress()
.width(40)
.height(40)
.margin({ top: 20 });
}
// 注册入口
Text('还没有账号?立即注册')
.fontSize(14)
.color('#007AFF')
.margin({ top: 50 })
.onClick(() => router.pushUrl({ url: Config.ROUTE_REGISTER }));
}
.width('100%')
.height('100%')
.backgroundColor('#f5f5f5')
.padding({ left: 30, right: 30 });
}
// 渲染QQ登录表单
@Builder renderQQForm() {
TextInput()
.hint('请输入QQ号')
.value(this.qqNumber)
.onChange((val) => this.qqNumber = val)
.style(CommonStyles.inputStyle);
TextInput()
.hint('请输入密码')
.value(this.qqPwd)
.type(InputType.Password)
.onChange((val) => this.qqPwd = val)
.style(CommonStyles.inputStyle)
.margin({ top: 20 });
}
// 渲染手机号登录表单
@Builder renderPhoneForm() {
TextInput()
.hint('请输入手机号')
.value(this.phoneNumber)
.onChange((val) => this.phoneNumber = val)
.style(CommonStyles.inputStyle);
Row() {
TextInput()
.hint('请输入验证码')
.value(this.verifyCode)
.onChange((val) => this.verifyCode = val)
.style(CommonStyles.inputStyle)
.flexGrow(1);
Button(this.countDown > 0 ? `${this.countDown}s后重发` : '获取验证码')
.width(120)
.height(50)
.fontSize(14)
.disabled(this.countDown > 0 || !/^1[3-9]\d{9}$/.test(this.phoneNumber))
.onClick(() => this.sendVerifyCode())
.margin({ left: 10 });
}
.margin({ top: 20 });
}
// 表单校验
private checkFormValid(): boolean {
if (this.loginType === 'qq') {
return /^\d{5,15}$/.test(this.qqNumber) && this.qqPwd.length >= 6;
} else {
return /^1[3-9]\d{9}$/.test(this.phoneNumber) && /^\d{6}$/.test(this.verifyCode);
}
}
// 发送验证码
private async sendVerifyCode() {
this.countDown = 60;
const timer = setInterval(() => {
this.countDown--;
if (this.countDown <= 0) clearInterval(timer);
}, 1000);
try {
const res = await RequestUtil.sendCode(this.phoneNumber);
if (!res.success) {
prompt.showToast({ message: res.message });
this.countDown = 0;
}
} catch (error) {
prompt.showToast({ message: '验证码发送失败' });
this.countDown = 0;
}
}
// 核心登录逻辑
private async handleLogin() {
this.isLoading = true;
try {
let res: LoginResponse;
// 区分登录类型
if (this.loginType === 'qq') {
res = await RequestUtil.qqLogin(this.qqNumber, this.qqPwd);
} else {
res = await RequestUtil.phoneLogin(this.phoneNumber, this.verifyCode);
}
// 登录成功处理
if (res.success && res.data?.token) {
// 1. 存储登录状态(内存+本地持久化)
AppStorage.setOrCreate('token', res.data.token);
AppStorage.setOrCreate('userInfo', res.data.user);
await StorageUtil.set('token', res.data.token);
await StorageUtil.set('userInfo', res.data.user);
// 2. 跳转页面(新用户引导/首页)
if (res.data.user.isNewUser) {
await router.pushUrl({ url: Config.ROUTE_GUIDE });
} else {
await router.pushUrl({ url: Config.ROUTE_MESSAGE });
router.clear(); // 清除登录页,避免返回
}
prompt.showToast({ message: '登录成功' });
} else {
prompt.showToast({ message: res.message || '登录失败' });
}
} catch (error) {
const errMsg = error instanceof Error ? error.message : '登录异常';
prompt.showToast({ message: errMsg });
} finally {
this.isLoading = false;
}
}
}
(4)开始登录时的界面:

2. 首页核心功能(登录后辐射场景)
(1)消息列表页(MessagePage.ets)
登录成功后默认进入消息列表,拉取当前用户的聊天会话:
import { RequestUtil } from '../../common/utils/RequestUtil';
import { MessageModel } from '../../models/MessageModel';
import { CommonStyles } from '../../common/styles/CommonStyles';
@Component
export struct MessagePage {
@State messageList: MessageModel[] = [];
@State isLoading: boolean = true;
async aboutToAppear() {
// 拉取消息列表
await this.fetchMessageList();
}
private async fetchMessageList() {
try {
const res = await RequestUtil.request<{ list: MessageModel[] }>('/message/list', {
method: http.RequestMethod.GET
});
this.messageList = res.list;
} catch (error) {
prompt.showToast({ message: '消息加载失败' });
} finally {
this.isLoading = false;
}
}
build() {
Column() {
// 顶部导航栏
Row() {
Text('消息')
.fontSize(22)
.fontWeight(FontWeight.Bold);
Image($r('app.media.plus'))
.width(24)
.height(24)
.margin({ left: 'auto' });
}
.padding(15)
.width('100%');
// 消息列表
if (this.isLoading) {
Progress().margin({ top: 50 });
} else {
List() {
ForEach(this.messageList, (item) => {
ListItem() {
Row() {
// 头像
Image(item.avatar || $r('app.media.default_avatar'))
.width(50)
.height(50)
.borderRadius(25);
// 消息内容
Column() {
Text(item.nickname)
.fontSize(16)
.fontWeight(FontWeight.Medium);
Text(item.lastMessage)
.fontSize(14)
.color('#999')
.maxLines(1)
.width(200);
}
.margin({ left: 15 })
.alignItems(HorizontalAlign.Start);
// 时间
Text(item.time)
.fontSize(12)
.color('#999')
.margin({ left: 'auto' });
}
.padding(15)
.width('100%')
.onClick(() => {
// 跳转到聊天页
router.pushUrl({ url: '/pages/home/ChatPage', params: { userId: item.userId } });
});
}
});
}
.width('100%')
.flexGrow(1);
}
}
.width('100%')
.height('100%')
.backgroundColor('#f5f5f5');
}
}
(2)个人中心页(MinePage.ets)
展示登录用户信息,支持退出登录(清除状态):
import AppStorage from '@ohos.app.ability.AppStorage';
import router from '@ohos.router';
import { StorageUtil } from '../../common/utils/StorageUtil';
import { Config } from '../../common/constants/Config';
import { CommonStyles } from '../../common/styles/CommonStyles';
@Component
export struct MinePage {
@State userInfo = AppStorage.get<any>('userInfo') || {};
build() {
Column() {
// 个人信息头部
Stack() {
Image($r('app.media.mine_bg'))
.width('100%')
.height(200);
Column() {
Image(this.userInfo.avatar || $r('app.media.default_avatar'))
.width(80)
.height(80)
.borderRadius(40)
.border({ width: 2, color: '#fff' });
Text(this.userInfo.nickname || '未知用户')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.color('#fff')
.margin({ top: 10 });
Text(`QQ号:${this.userInfo.qqNumber || '未绑定'}`)
.fontSize(14)
.color('#fff')
.margin({ top: 5 });
}
.alignItems(HorizontalAlign.Center)
.margin({ top: 60 });
}
// 功能列表
List() {
ListItem() {
this.renderSettingItem('设置', 'app.media.setting', () => {
router.pushUrl({ url: Config.ROUTE_SETTING });
});
}
ListItem() {
this.renderSettingItem('退出登录', 'app.media.logout', () => {
this.handleLogout();
});
}
}
.width('100%')
.margin({ top: 20 });
}
.width('100%')
.height('100%');
}
@Builder renderSettingItem(title: string, icon: string, onClick: () => void) {
Row() {
Image($r(`app.media.${icon}`))
.width(24)
.height(24);
Text(title)
.fontSize(16)
.margin({ left: 15 });
Image($r('app.media.arrow_right'))
.width(16)
.height(16)
.margin({ left: 'auto' });
}
.padding(15)
.width('100%')
.onClick(onClick);
}
// 退出登录
private async handleLogout() {
// 清除登录状态
AppStorage.delete('token');
AppStorage.delete('userInfo');
await StorageUtil.clear('token');
await StorageUtil.clear('userInfo');
// 跳回登录页
router.replaceUrl({ url: Config.ROUTE_LOGIN });
prompt.showToast({ message: '已退出登录' });
}
}
三、鸿蒙开发核心坑点与解决方案
1. 登录相关高频问题
|
问题现象 |
根本原因 |
解决方案 |
|
net::ERR_CLEARTEXT_NOT_PERMITTED |
鸿蒙禁止HTTP明文传输,配置文件错误 |
检查network_security_config.json语法(无注释),module.json5引用路径正确 |
|
Usage of standard library is restricted |
使用ArkTS受限API(如await同步方法) |
AppStorage.setOrCreate是同步方法,移除await;避免Promise.allSettled等API |
|
登录成功后重启App需重新登录 |
仅用AppStorage(内存存储),未持久化 |
配合Preferences封装StorageUtil,登录状态同步存储到本地文件 |
|
Token失效后页面无响应 |
未处理401状态码 |
拦截器中捕获401,自动清除状态并跳回登录页 |
五、总结
本文基于鸿蒙ArkTS的entry工程,完整实现了模拟QQ App的核心功能,重点拆解了登录体系的开发与坑点解决方案,同时覆盖了消息列表、个人中心等核心场景。开发过程中需重点关注「鸿蒙网络安全规范、ArkTS标准库限制、登录状态持久化」三大核心点,既能保证功能正常运行,也能符合鸿蒙应用的开发规范。
该项目的架构设计与代码实现可直接复用至各类鸿蒙社交类App开发,开发者可基于此扩展更多QQ特色功能(如群聊、表情、文件传输等),深入掌握鸿蒙应用开发的核心逻辑。
完整项目代码已同步至 Gitee(地址:),欢迎 Star 交流!
更多推荐


所有评论(0)