从零搭建仿微信聊天应用并发布到华为云开发者展览馆的实战经验
从零搭建仿微信聊天应用并发布到华为云开发者展览馆的实战经验
作者:华为云 IaC 架构伙伴
发布日期:2026-10-16
阅读时间:约 12 分钟
标签:前端开发、纯HTML/CSS/JS、华为云、DevBridge、GitCode
一、项目背景与目标
为什么做仿微信聊天应用
微信是中国用户最熟悉的移动应用之一,它的聊天界面几乎成了"即时通讯"的视觉标准。选择仿微信作为练手项目,有两个好处:一是目标清晰——每个人都知道微信长什么样,验收时不需要反复确认"这样对不对";二是覆盖面广——列表页、详情页、导航栏、表单输入、状态管理,一个聊天应用把这些前端核心场景全包了。
技术选型:为什么选纯 HTML+CSS+JS
这个决定不是"反框架",而是基于场景的务实选择:
| 考量维度 | 纯 HTML/CSS/JS | Vue/React |
|---|---|---|
| 项目规模 | 单文件,889 行 | 需要构建工具链 |
| 部署复杂度 | python3 -m http.server 即可 |
需要 npm build + 静态服务器 |
| 预览速度 | 秒开 | 首次构建 + 启动 |
| 代码可读性 | 打开即读,无编译层 | 需要理解框架抽象 |
对于一个演示性质的原型应用,纯前端方案的最大优势是零依赖、零构建、零配置。所有代码内联在单个 index.html 中,任何人拿到文件双击就能打开,这在分享和验收时极为高效。
目标用户与使用场景
- 目标用户:普通用户,期望在手机端体验类微信聊天
- 使用场景:技术原型展示、前端学习参考、华为云开发者展览馆投稿
二、架构设计
单页应用结构
整个应用是一个单页应用(SPA),但没用任何路由库。核心思路是:所有页面预先写在 DOM 中,通过 display: none / block 切换可见性。
┌─────────────────────────────┐
│ 手机外框 │
│ ┌───────────────────────┐ │
│ │ 状态栏 │ │
│ ├───────────────────────┤ │
│ │ │ │
│ │ 页面内容区(四选一) │ │
│ │ - 聊天列表页 │ │
│ │ - 通讯录页 │ │
│ │ - 发现页 │ │
│ │ - 我的页 │ │
│ │ │ │
│ ├───────────────────────┤ │
│ │ 聊天详情页(覆盖层) │ │
│ ├───────────────────────┤ │
│ │ 底部导航栏 │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
五大功能模块
| 模块 | 职责 | 关键交互 |
|---|---|---|
| 聊天列表 | 展示会话列表 | 点击进入详情 |
| 聊天详情 | 消息收发 | 发送、表情、自动回复 |
| 通讯录 | 联系人管理 | 字母分组、发起聊天 |
| 发现 | 功能入口 | 朋友圈、扫一扫等 |
| 我的 | 个人信息 | 头像、设置项 |
数据层设计
没有用 Redux/Vuex 之类的状态管理,而是用最朴素的方式:一个全局对象持有所有状态,函数直接操作这个对象然后重新渲染对应 DOM 节点。
const state = {
currentTab: 'chat',
currentChatId: null,
chats: [{
id: 'chat_001',
name: '张三',
avatar: { color: '#07C160', text: '张' },
messages: [
{ type: 'text', content: '在吗?', fromMe: false, time: '14:30' },
{ type: 'text', content: '在的', fromMe: true, time: '14:31' }
],
unread: 2
}],
contacts: [/* 24 位联系人 */]
};
页面路由方案
不依赖 hash 路由或 History API,而是用函数直接控制 DOM:
function switchTab(tabName) {
document.querySelectorAll('.page').forEach(p => p.style.display = 'none');
document.querySelector(`.${tabName}-page`).style.display = 'block';
state.currentTab = tabName;
}
function openChat(chatId) {
state.currentChatId = chatId;
renderChatDetail(chatId);
document.querySelector('.chat-detail-page').style.display = 'flex';
}
💡 技术要点:这种"显隐式路由"在小型 SPA 中完全够用。缺点是没有浏览器前进/后退支持,但对于手机端模拟应用来说,用户不会期望按浏览器返回键。
三、核心功能实现
3.1 聊天列表页
会话数据结构设计为"每个会话内嵌完整消息数组",而不是"会话和消息分两张表"。对于纯前端原型,内嵌结构省去了关联查询的麻烦,渲染时一次遍历即可。
function renderChatList() {
const list = document.querySelector('.chat-list');
list.innerHTML = state.chats.map(chat => {
const lastMsg = chat.messages[chat.messages.length - 1];
return `
<div class="chat-item" onclick="openChat('${chat.id}')">
<div class="avatar" style="background:${chat.avatar.color}">
${chat.avatar.text}
</div>
<div class="chat-info">
<span class="chat-name">${chat.name}</span>
<span class="chat-time">${lastMsg.time}</span>
<span class="chat-preview">${lastMsg.content}</span>
${chat.unread > 0 ? `<span class="unread-badge">${chat.unread}</span>` : ''}
</div>
</div>`;
}).join('');
}
未读角标的计算很简单:每个会话维护一个 unread 计数器,进入聊天详情时清零。底部导航栏微信 tab 的角标是所有会话未读数之和。
3.2 聊天详情页
消息气泡的 CSS 实现是聊天界面的核心视觉元素:
/* 己方消息:靠右,绿色背景 */
.bubble-self {
background: #07C160; /* 微信经典绿 */
color: #fff;
border-radius: 4px 0 4px 4px;
margin-right: 10px;
}
/* 对方消息:靠左,白色背景 */
.bubble-other {
background: #fff;
color: #333;
border-radius: 0 4px 4px 4px;
margin-left: 10px;
}
💡 设计细节:气泡的圆角不是四角相同,而是"尖角对侧"的三个角圆化,模拟微信的尖角指向头像方向。这个细节很小,但缺了它就会"不像微信"。
消息发送与自动滚动:
function sendMessage(content) {
const chat = state.chats.find(c => c.id === state.currentChatId);
chat.messages.push({
type: 'text', content, fromMe: true, time: formatTime(new Date())
});
renderMessages(chat);
scrollToBottom();
// 模拟对方回复(500-1500ms 延迟)
setTimeout(() => autoReply(chat, content), 500 + Math.random() * 1000);
}
模拟自动回复策略:关键词匹配优先,无匹配时随机回复。
const replyRules = [
{ keywords: ['你好', '在吗'], replies: ['在的,怎么了?', '你好呀~'] },
{ keywords: ['再见', '拜拜'], replies: ['好的,下次聊', '拜拜~'] },
{ keywords: ['谢谢', '感谢'], replies: ['不客气', '应该的'] },
];
const randomReplies = ['嗯嗯', '好的', '我知道了', '哈哈', '说说看'];
function autoReply(chat, userMessage) {
let reply = null;
for (const rule of replyRules) {
if (rule.keywords.some(kw => userMessage.includes(kw))) {
reply = rule.replies[Math.floor(Math.random() * rule.replies.length)];
break;
}
}
if (!reply) {
reply = randomReplies[Math.floor(Math.random() * randomReplies.length)];
}
chat.messages.push({
type: 'text', content: reply, fromMe: false, time: formatTime(new Date())
});
renderMessages(chat);
scrollToBottom();
}
3.3 通讯录与字母索引
联系人按拼音首字母分组排序:
function renderContacts() {
const grouped = {};
state.contacts.forEach(contact => {
const letter = contact.pinyin[0].toUpperCase();
if (!grouped[letter]) grouped[letter] = [];
grouped[letter].push(contact);
});
const letters = Object.keys(grouped).sort();
// 按 A-Z 分组渲染...
}
点击联系人时,检查是否已有该联系人的聊天会话——有则直接打开,没有则新建一个会话再打开。这个跳转逻辑让通讯录和聊天列表形成了闭环。
3.4 手机外框与状态栏
用 CSS 模拟手机外观是"第一眼像微信"的关键:
.phone-frame {
width: 375px;
height: 667px;
border-radius: 40px;
box-shadow:
0 0 0 12px #1a1a1a, /* 黑色边框 */
0 0 0 14px #2a2a2a, /* 边框高光 */
0 20px 60px rgba(0,0,0,0.5); /* 投影 */
overflow: hidden;
}
/* 听筒凹槽 */
.phone-frame::before {
content: '';
position: absolute;
top: 0; left: 50%;
transform: translateX(-50%);
width: 150px; height: 22px;
background: #1a1a1a;
border-radius: 0 0 18px 18px;
}
状态栏实时时间用 setInterval 每秒更新,显示时分 + 5G 信号 + 电池图标。
四、交互细节与用户体验
消息时间格式化
function formatTime(date) {
const now = new Date();
const diff = (now - date) / 1000;
if (diff < 60) return '刚刚';
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
const h = String(date.getHours()).padStart(2, '0');
const m = String(date.getMinutes()).padStart(2, '0');
if (date.toDateString() === now.toDateString()) return `${h}:${m}`;
return `${date.getMonth() + 1}月${date.getDate()}日 ${h}:${m}`;
}
头像生成方案
用"彩色圆形 + 姓名首字"代替图片。颜色从预设调色板中按姓名 hash 分配,保证同一人始终拿到同一颜色:
const avatarColors = ['#07C160', '#FA5151', '#FFC300', '#576B95',
'#10AEFF', '#FA9D3B', '#C490BF', '#67C23A'];
function getAvatarColor(name) {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return avatarColors[Math.abs(hash) % avatarColors.length];
}
移动端适配
<meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, user-scalable=no">
user-scalable=no 禁止双指缩放——在模拟手机应用的场景下,允许缩放会破坏"像 App"的错觉。同时全局加上 -webkit-tap-highlight-color: transparent 消除 iOS 点击灰色高亮。
五、发布到华为云开发者展览馆
应用写完后,通过华为云 Agent Skills 自动化发布到开发者展览馆。整个发布流程是一个 8 步 Pipeline:
Step 1: 解析 IAM Domain ID + 生成 STS 临时凭证
Step 2: 启动项目 + 创建 DevBridge 隧道
Step 3: Git 仓库信息 + 作品命名
Step 4: 封面图自动生成(Playwright 截图 + 合成)
Step 5: 生成一句话简介
Step 6: 作品详情打包(架构图 + README + zip)
Step 7: 选择投稿活动
Step 8: 发布 + 积分奖励
IAM 域名解析与 STS 临时凭证
发布前需要获取华为云账号的身份凭证。通过 hcloud CLI 调用 IAM KeystoneListAuthDomains 获取 Domain ID,然后创建自委托生成 STS 临时凭证(有效期 900 秒)。
# 解析 Domain ID
hcloud IAM KeystoneListAuthDomains
# → domain_id: c54f63278482423cae2d660d0b352f29
# 生成 STS 临时凭证
python3 gen_sts.py --account <domainID> \
--creds-out ./sts-creds.json --sh-out ./sts-creds.sh
⚠️ 安全要点:永久 AK/SK 留在本地不上传,只用 STS 临时凭证(15 分钟有效)与平台交互。
GitCode 仓库创建与代码推送
通过 GitCode OAuth 授权后,调用 API 创建仓库并推送代码:
# 创建仓库
curl -X POST "https://gitcode.com/api/v5/user/repos" \
-H "Authorization: Bearer ${TOKEN}" \
-d '{"name":"wechat-chat","private":false}'
# 推送代码
git remote add origin https://gitcode.com/<user>/wechat-chat.git
git push -u origin master
DevBridge 开发隧道
DevBridge 是华为云的开发隧道服务,能把本地端口安全暴露到公网:
# 创建隧道 + 配置端口 + 启动托管
TUNNEL_ID=$(db_create "wechat-tunnel" "开发隧道" 72)
db_port_create "$TUNNEL_ID" 8080 auto true
db_host "$TUNNEL_ID"
TUNNEL_URL=$(db_tunnel_url "$TUNNEL_ID")
# → https://nx6f7rhz-8080.devbridge-s2.hwtunnel.com
封面图自动生成
封面图不是手动截图,而是通过 Playwright 自动化生成:
- preflight:安装 CJK 字体 + Chromium + Python 依赖(首次约 2-5 分钟)
- screenshot:Playwright 打开应用 URL,自动截图
- compose:将截图与标题、简介合成为 16:9 封面图
- verify:像素级字形校验,防止中文字体缺失导致"豆腐块"
build-cover.mjs \
--url "http://127.0.0.1:8080" \
--title "仿微信聊天应用" \
--out /tmp/cover.png
作品详情打包
详情包包含一篇 500-1500 字的详解文章 + 系统架构图(Playwright 渲染 HTML 生成 PNG),打包为 zip 上传:
# 生成架构图
generate_diagram.py --html diagram.html --out architecture.png
# 打包详情 zip
build-detail-zip.mjs \
--readme detail-readme.md \
--resources /tmp/work-resources \
--out /tmp/detail.zip
最终发布
所有产物就绪后,调用发布 API:
publish-work.mjs /tmp/publish-params.json
# → #status=201
# → workId=0061bb76e6800000
# → workUrl=https://gallery.developer.huaweicloud.com/gallery/0061bb76e6800000
# → reward=success value=200
发布成功后获得 200 积分奖励,作品在展览馆公开展示。
六、技术心得与总结
纯前端单页应用的优劣势
优势:
- 零依赖、零构建、零配置,上手门槛极低
- 单文件分发,复制即用
- 代码即文档,没有抽象层,读代码就能理解全部逻辑
劣势:
- 规模一上来,单文件会变得难以维护(889 行已经接近舒适上限)
- 没有组件复用机制,相似 UI 结构只能复制粘贴
- 缺少类型检查,重构时容易遗漏
💡 结论:纯前端方案适合 1000 行以内的原型/演示项目。超过这个规模,引入框架的收益开始大于成本。
无框架开发的体验反思
不用框架写 SPA,最深的感受是"模板字符串拼 HTML"的痛。每次 innerHTML = state.map(...).join('') 都在重复"数据→字符串→DOM"的转换,而框架的声明式模板把这件事封装得干净得多。
但也有爽的地方:没有虚拟 DOM 的抽象层,scrollTop = scrollHeight 这种直接操作 DOM 的代码写起来毫无心理负担。调试时打开 DevTools,所有状态都在全局对象上,一目了然。
华为云开发者生态工具链体验
这次发布流程让我对华为云开发者生态有了完整体验:
- CodeArts ACP:通过 ACP 协议让 CodeArts 在沙箱里写代码,全程自动化,不需要手动建项目、装依赖
- GitCode:OAuth 授权 + API 建仓库 + git push,代码托管一条龙
- DevBridge:开发隧道让本地服务秒变公网可访问,对外分享原型非常方便
- 展览馆发布 Pipeline:8 步流程从 IAM 认证到封面生成到最终发布,每个环节都有脚本封装,Agent 只需按步骤调用
整个流程中,最耗时的是 preflight 首次安装 Chromium + CJK 字体(约 2-5 分钟),之后所有截图/封面生成都复用这套环境,秒级完成。
对后续开发的建议
- 原型阶段用纯前端,产品化时迁移框架:先用纯 HTML/CSS/JS 快速验证交互逻辑,确认方案可行后再迁移到 Vue/React,避免过早引入构建复杂度
- 关注视觉细节:气泡圆角方向、头像颜色一致性、消息时间格式化——这些"不起眼"的细节累积起来,决定了用户是否觉得"像"
- 善用 Agent Skills 自动化:发布到展览馆的 8 步流程如果手动操作至少半小时,用 Skill 封装后一条指令搞定,把时间留给更有价值的创造
- 点赞
- 收藏
- 关注作者
评论(0)