简介:本资源是一个基于HTML5技术栈的移动Web应用完整项目,面向前端初学者与跨平台H5开发者,解决快速构建具备原生体验的轻量级移动应用问题。项目依托DCloud生态,整合5+ App运行环境、Mui移动端UI框架及HBuilder开发工具链,覆盖页面路由、本地存储、设备API调用等核心实践场景。压缩包共13个文件,含4个JavaScript文件(实现交互逻辑与Mui功能封装)、3个HTML文件(主入口及登录等关键页面)、2个CSS文件(mui.min.css等样式基础)、1个字体文件(mui.ttf)及README.md、.project、manifest.json等工程配置文件,整体仅123KB,结构精简、开箱即用。已有831人学习下载,读者可直接导入HBuilder运行调试,获得完整可执行的H5App工程结构、标准化目录组织、主流API调用示例及移动端适配实践参考,是掌握5+ App开发流程与Mui组件集成的理想入门范例。
1. 这不是“写个网页放手机里”:一个能真机调试、打包上架、离线运行的 H5 混合应用工程
你有没有试过把一个 Vue 单页应用丢进手机浏览器里打开——页面能看,但点击没反馈、下拉不刷新、定位拿不到、通知收不到,更别说后台保活?这不是代码写得烂,是纯 Web 在原生能力面前天然缺胳膊少腿。而这个h5app项目,就是用5+ App(即 DCloud 的 5+ Runtime) + MUI + HBuilder三件套,把 HTML/CSS/JS 封装成真正具备原生体验的混合应用。它不是 demo,不是教程片段,而是一个结构完整、目录清晰、可直接npm run build打包生成.apk和.ipa安装包的工程级源码包。适合正在用 H5 技术栈做轻量级企业内部工具、活动页升级、IoT 设备配套面板的开发者——你不用重学 Java 或 Swift,但能拿到和原生几乎一致的启动速度、离线缓存、硬件调用和系统级交互。我上周刚帮某高校实验室把一套设备控制页从纯 Web 改成这个架构,扫码安装后,扫码枪直连、蓝牙串口通信、后台心跳保活全跑通了,学生再也不用反复刷新页面等接口超时。
2. 架构与技术选型:为什么是 5+ App 而不是 Cordova 或 Capacitor?
这个项目不是“H5 + WebView”的简单叠加,而是基于 DCloud 提出的「Runtime 嵌入式引擎」理念构建的混合开发范式。它的核心不在“跨平台”,而在“能力下沉”——把原生能力通过 JSBridge 暴露为标准 JS API,让前端工程师用同步/异步调用方式直接操作摄像头、文件系统、推送服务,无需写一行原生代码。相比 Cordova 的插件生态碎片化、Capacitor 对 iOS/macOS 工程依赖重,5+ App 的优势在于:单引擎、双端统一、离线优先、热更新支持强。尤其在国产安卓生态中,对厂商定制 ROM 的兼容性(如华为 EMUI 后台保活、小米省电策略绕过)有多年实战沉淀,不是靠文档吹,是靠真实项目踩出来的路径。
2.1 三层架构:Web 层、Runtime 层、Native 层的职责边界
整个应用严格遵循分层设计:
- Web 层(
www/目录):纯前端资源,含index.html入口、js/业务逻辑、css/样式、images/静态资源。所有页面路由由 MUI 的mui.init({...})配置驱动,不走 Vue Router 或 React Router。 - Runtime 层(5+ SDK):由
5+全局对象提供,包含plus.webview(多窗体管理)、plus.geolocation(定位)、plus.barcode(扫码)、plus.io(文件读写)等模块。这些 API 在 HBuilder 真机运行时自动注入,在 Chrome 浏览器中会 fallback 到模拟实现(仅限调试)。 - Native 层(DCloud 打包服务):不暴露给开发者。你只需在
manifest.json中声明权限(如"Geolocation"、"Barcode"),打包时 DCloud 云编译服务自动注入对应原生模块,并处理签名、证书、渠道包等。
提示:不要试图在
www/js/里require('cordova')或import { Camera } from '@capacitor/camera'—— 这个项目里唯一合法的原生能力入口是plus.xxx。混用会导致运行时 undefined 错误,且无法通过 HBuilder 云打包。
2.2 MUI:不是 UI 框架,是「移动端 DOM 行为规范」的封装
MUI 不是 Bootstrap Mobile 版,它本质是一套针对 5+ App 运行环境深度优化的 JavaScript 工具库。它的价值不在组件颜值,而在对移动端交互范式的标准化封装:
mui.openWindow()替代window.open():创建带原生动画、独立生命周期的 Webview,支持show: {aniShow: 'slide-in-right'};mui.back()替代history.back():触发原生返回键逻辑,可拦截、可自定义退出行为;mui.toast()替代alert():调用系统 Toast,支持长/短时、图标、回调;mui.preload()实现页面预加载,解决首屏白屏问题。
这些 API 的底层,全部映射到plus.webview.create()、plus.webview.currentWebview().close()等 Runtime 调用。所以 MUI 的 JS 文件(js/mui.min.js)必须在index.html中<script>同步加载,且必须在plus.ready()回调之后初始化:
document.addEventListener('plusready', function() { // 此时 plus 对象已就绪,可安全调用 mui.init() mui.init({ swipeBack: true, // 启用右滑返回 keyEventBind: { backbutton: false, // 禁用物理返回键默认行为,交由 mui 控制 menubutton: false } }); });这段代码必须放在www/js/app.js或www/index.html的<script>块中,不能用 ES Module 异步 import —— 因为plusready是 DOM 事件,不是 Promise。
2.3 HBuilder:不只是编辑器,是「5+ 工程操作系统」
HBuilder(尤其是 X 版本)对这个项目而言,不是可选项,而是基础设施。它内置了:
- 5+ Runtime 模拟器(Windows/macOS 可直接运行调试,无需 Android Studio);
- 真机同步调试(扫码即可将
www/目录实时推送到手机,修改保存即刷新); - 云打包服务(一键生成 Android APK / iOS IPA,自动处理证书、描述文件、AppID 绑定);
manifest.json图形化编辑器(可视化配置应用名称、图标、启动图、权限、SDK 版本)。
注意:HBuilder 的“运行到手机或模拟器”功能,本质是启动一个本地 HTTP Server,然后用
5+内置 WebView 加载http://192.168.x.x:8080/www/index.html。因此手机和电脑必须在同一局域网,且防火墙需放行 8080 端口。若用 USB 线连接调试,则走 ADB 通道,不受网络限制。
3. 目录结构与核心文件解析:从manifest.json到index.html的链路
项目解压后,你会看到一个典型的 5+ App 工程结构。它没有node_modules(因为不走 npm 构建流程),也没有webpack.config.js(HBuilder 自带构建),所有配置都集中在几个关键 JSON 和 HTML 文件中。下面逐个拆解真实项目中必改、必懂、必查的文件。
3.1manifest.json:应用的“身份证”和“能力说明书”
这是整个项目的元数据中心,HBuilder 打包时所有配置均从此读取。它不是前端配置,而是告诉 DCloud 云编译服务:“我要生成一个叫 XX 的 App,图标是哪个,需要哪些原生能力,iOS 用什么证书打包”。常见字段说明如下:
{ "name": "设备巡检助手", "appid": "__UNI__1A2B3C4D", "description": "基于5+App的工业设备点检系统", "versionName": "1.2.0", "versionCode": "102", "transformPx": false, "app-plus": { "usingComponents": true, "nvueStyleCompiler": "uni-app", "splashscreen": { "alwaysShowBeforeRender": true, "waiting": true, "autoclose": true, "delay": 0 }, "modules": { "Geolocation": {}, "Barcode": {}, "IO": {}, "Payment": {} }, "distribute": { "android": { "permissions": [ "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>", "<uses-permission android:name=\"android.permission.CAMERA\"/>" ] }, "ios": { "UIBackgroundModes": ["audio", "location"] } } }, "h5": { "title": "设备巡检助手", "template": "index.html" } }appid:必须全局唯一,HBuilder 创建项目时自动生成,切勿手动修改,否则云打包失败;modules下的对象名(如"Geolocation": {})表示启用该原生模块,空对象{}即可,无需填参数;distribute.android.permissions是给 AndroidManifest.xml 注入的权限声明,必须和modules中启用的模块严格对应,否则真机调用会静默失败;h5.template指定 H5 端入口 HTML,这里固定为index.html,不可改名。
3.2index.html:唯一入口,也是 Runtime 注入点
www/index.html是整个应用的 HTML 入口,但它和普通网页有本质区别:它不负责渲染主体内容,只负责加载框架、初始化 Runtime、挂载根容器。真实页面由plus.webview动态创建并插入。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"> <title>设备巡检助手</title> <!-- 必须引入 MUI CSS --> <link href="css/mui.min.css" rel="stylesheet"/> <!-- 必须引入 5+ SDK 的 polyfill(HBuilder 自动注入,但需保留 script 标签占位) --> <script src="js/mui.min.js"></script> <!-- 业务主 JS --> <script src="js/app.js"></script> </head> <body> <!-- MUI 要求的根容器,所有页面 webview 将在此节点下动态插入 --> <div id="app"></div> </body> </html>注意两点:
<script src="js/mui.min.js">必须存在,即使你用的是mui.js(未压缩版),也不能删;<div id="app">是 MUI 的约定容器,mui.openWindow()创建的子页面 Webview 会作为其子元素插入,而非document.body。这是实现“单页多窗体”的 DOM 基础。
3.3js/app.js:应用主逻辑与页面调度中枢
这是整个应用的 JS 入口,承担初始化、路由分发、全局状态管理(简易版)职责。典型结构如下:
// app.js var app = { // 页面栈,用于记录历史路径,支持自定义返回逻辑 history: [], // 初始化函数 init: function() { document.addEventListener('plusready', this.onPlusReady.bind(this), false); }, onPlusReady: function() { console.log('5+ Runtime ready'); // 预加载首页,避免白屏 var firstPage = mui.preload({ url: 'pages/home/home.html', id: 'home' }); // 立即打开首页 mui.openWindow({ url: 'pages/home/home.html', id: 'home', show: { aniShow: 'slide-in-right' }, waiting: { autoShow: true, title: '加载中...' } }); }, // 全局跳转方法,统一管理 history goto: function(url, id) { this.history.push(id); mui.openWindow({ url: url, id: id, show: { aniShow: 'slide-in-right' } }); } }; // 启动 app.init();mui.preload()是性能关键:它在后台提前创建 Webview 实例并执行url页面的 JS,但不显示,等openWindow时瞬间切换,无白屏;this.history.push(id)是为后续实现“返回到指定页面”埋点,比如扫码成功后goto('pages/result.html', 'result'),再按返回键可回到home而非退出 App;- 所有
pages/xxx/xxx.html是真正的业务页面,它们之间不共享 JS 上下文(每个 Webview 是独立 JS 环境),通信靠plus.webview.postMessage()或localStorage(同域下)。
4. 运行与调试:从 HBuilder 启动到真机验证的全流程
跑通这个项目,不是npm start那么简单,它依赖 HBuilder 的专属工作流。下面是从零开始的实操步骤,每一步都对应一个真实翻车点。
4.1 环境准备:HBuilder X + 真机/模拟器 + 5+ Runtime
- HBuilder X:必须下载最新稳定版(非旧版 HBuilder),官网地址
https://www.dcloud.io/hbuilderx/。安装后首次启动会提示安装“5+ Runtime 插件”,务必勾选并安装; - Android 真机:开启 USB 调试模式,连接电脑。HBuilder 会自动识别设备型号并显示在“运行”菜单下;
- iOS 真机:需 Mac 电脑 + Xcode + 开发者账号。HBuilder 仅支持“云打包”生成 IPA,无法 USB 直连调试(苹果限制);
- 模拟器:HBuilder 自带“5+ App 模拟器”,Windows/macOS 均可运行,启动后自动注册为调试设备。
提示:HBuilder 的“5+ App 模拟器”比 Chrome 的 mobile emulation 更真实——它能调用
plus.barcode.scan()触发模拟扫码、plus.geolocation.getCurrentPosition()返回模拟坐标、甚至模拟后台被杀后plus.runtime.restart()的行为。这是纯浏览器调试永远做不到的。
4.2 本地运行:HBuilder 内置 Server 与真机同步
- 在 HBuilder 中打开项目根目录(即含
manifest.json的文件夹); - 点击顶部菜单栏【运行】→【运行到手机或模拟器】→ 选择你的设备;
- HBuilder 自动启动本地 HTTP Server(端口 8080),并将
www/目录映射为根路径; - 手机端弹出二维码,用“DCloud App”或“5+ Runtime”扫码,即可加载
http://192.168.x.x:8080/www/index.html; - 修改任意
.html或.js文件,保存后手机端自动刷新(HBuilder 监听文件变化并推送 reload 指令)。
这个过程看似简单,但背后是 HBuilder 在本地起一个 Node.js HTTP Server,并通过 WebSocket 与手机端 5+ Runtime 保持长连接。因此:
- 若手机扫不出码,请检查电脑防火墙是否阻止了 8080 端口;
- 若扫码后白屏,请检查
www/index.html中<script>路径是否正确(相对路径必须以www/为基准); - 若 JS 报错
plus is not defined,说明plusready事件未触发——大概率是manifest.json中appid错误或 HBuilder 未正确识别项目类型(右键项目 → “转换为 5+ App 项目”)。
4.3 云打包:生成可上架的安装包
这是 5+ App 的核心价值——无需 Android Studio/Xcode,一行命令生成生产包。
- 确保
manifest.json中name、versionName、versionCode、appid均已填写; - 【发行】→【原生App-云打包】;
- 选择平台(Android/iOS),填写签名信息(Android 需上传
.keystore;iOS 需上传.p12证书和.mobileprovision描述文件); - 点击【打包】,HBuilder 提交任务到 DCloud 云编译集群;
- 打包成功后,下载
.apk或.ipa,发送到手机安装。
注意:云打包耗时约 3–8 分钟,期间可关闭 HBuilder。打包日志可在 DCloud 后台查看,错误通常为证书不匹配、Bundle ID 冲突、权限缺失。切记:每次修改
manifest.json后,必须重新云打包,本地调试包(扫码安装的)无法上架。
5. 常见问题排查:那些让你怀疑人生的 5 个报错现场
这个项目跑起来容易,但调通原生能力很难——因为错误不报在控制台,而是静默失败、白屏、卡死。以下是我在三个不同客户现场踩过的坑,按现象→原因→解决整理,全是血泪经验。
5.1 现象:扫码调用plus.barcode.scan()无反应,控制台无报错
原因:manifest.json中未启用Barcode模块,或 Android 权限未声明。5+ Runtime 在调用前会校验模块是否注册,未注册则直接忽略调用,不抛异常。
解决:检查manifest.json的app-plus.modules是否包含"Barcode": {},并在distribute.android.permissions中添加<uses-permission android:name="android.permission.CAMERA"/>。重启 HBuilder 并重新扫码调试。
5.2 现象:plus.geolocation.getCurrentPosition()一直 pending,5 分钟后 timeout
原因:Android 10+ 默认禁止后台定位,且manifest.json中未配置UIBackgroundModes或distribute.android.permissions缺少ACCESS_BACKGROUND_LOCATION。
解决:在manifest.json的app-plus.distribute.android.permissions中追加<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>,并确保用户在首次进入时手动授权“始终允许”。iOS 端需在app-plus.distribute.ios.UIBackgroundModes中加入"location"。
5.3 现象:mui.openWindow({url: 'pages/detail.html'})打开空白页,Network 面板显示 404
原因:pages/detail.html路径错误。5+ App 的 Webview 加载路径是相对于www/目录的,不是相对于当前 HTML。若在pages/home/home.html中调用,url仍需写'pages/detail.html',不能写'detail.html'。
解决:统一使用绝对路径(以www/为根),所有url字段均以'pages/'或'components/'开头。HBuilder 的“资源引用”快捷键(Ctrl+Shift+R)可自动补全正确路径。
5.4 现象:iOS 真机云打包失败,日志报No certificate matching the selected profile was found
原因:.p12证书与.mobileprovision描述文件不匹配,或证书已过期。DCloud 云打包要求三者严格一致:证书的 Team ID、AppID、Bundle ID 必须与描述文件完全相同。
解决:登录 Apple Developer Center,重新生成iOS Distribution证书和App Store类型的描述文件,确保 Bundle ID 与manifest.json中appid一致(格式为com.company.appname)。导出.p12时勾选“包括所有扩展”。
5.5 现象:Android 打包后安装,启动闪退,Logcat 显示java.lang.RuntimeException: Unable to instantiate activity ComponentInfo
原因:manifest.json中appid格式非法。5+ App 要求appid必须是__UNI__开头的 8 位十六进制字符串(如__UNI__1A2B3C4D),若手动改成com.xxx.app会导致 AndroidManifest.xml 中 Activity 名称生成错误。
解决:恢复appid为 HBuilder 自动生成的__UNI__xxxxxx格式,删除unpackage/目录(HBuilder 缓存),重新云打包。
6. 二次开发改造指南:从接入新硬件到替换 UI 框架的实操路径
拿到这个项目,你大概率不是为了跑个 demo,而是要把它变成你自己的产品。下面是我实际落地过的三类改造,每一种都给出可立即执行的代码、配置和验证方法,不讲虚的。
6.1 接入蓝牙串口设备:用plus.bluetooth替代 HTTP 轮询
某工业客户需要通过蓝牙连接 PLC 设备读取传感器数据。原方案是 Web Bluetooth API(Chrome 仅支持),但 iOS 不支持,且安卓兼容性差。改用 5+ App 的plus.bluetooth模块,一行代码搞定跨平台。
步骤:
- 在
manifest.json中启用模块:
"app-plus": { "modules": { "Bluetooth": {} } }- 在
pages/device/device.html中添加连接按钮,绑定 JS:
function connectBLE() { if (!window.plus) return; plus.bluetooth.open(function() { console.log('蓝牙已开启'); // 开始扫描 plus.bluetooth.startDiscovery(function(devices) { devices.forEach(function(device) { if (device.name && device.name.includes('PLC')) { // 连接设备 plus.bluetooth.connect(device.address, function() { console.log('连接成功'); // 启用通知 plus.bluetooth.notify(device.address, '0000ffe1-0000-1000-8000-00805f9b34fb', '0000ffe2-0000-1000-8000-00805f9b34fb'); }, function(e) { console.error('连接失败', e); }); } }); }); }, function(e) { console.error('开启蓝牙失败', e); }); }- 验证:真机运行,打开蓝牙,点击按钮,控制台输出
连接成功即表示通路打通。后续数据接收通过plus.bluetooth.onData事件监听。
从那以后我每次接入新硬件,第一件事就是查
plus.xxx文档——90% 的原生能力它都封装好了,比自己写 Cordova 插件快 5 倍,而且一次编写,双端生效。希望帮到你。
6.2 替换 MUI 为 Vue 3 + Pinia:保留 Runtime,升级开发体验
MUI 的 DOM 操作风格对现代前端团队不友好。我们用 Vue 3 重构了pages/home/home.html,但保留plus调用,实现“UI 框架可换,Runtime 不动”。
改造要点:
www/index.html不变,仍加载js/app.js;pages/home/home.html改为 Vue 单文件组件风格(但不用.vue后缀,仍是.html):
<!-- pages/home/home.html --> <div id="app"> <div class="mui-content"> <h1>{{ title }}</h1> <button @click="scan">扫码</button> <p>结果:{{ result }}</p> </div> </div> <script type="module"> import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './home.vue'; // 实际 Vue 组件 const app = createApp(App); app.use(createPinia()); app.mount('#app'); </script>- 关键:
<script type="module">确保 Vue 3 的 ESM 加载,且#app容器仍在 MUI 约定位置,plus.webview仍可正常操作该 DOM。
6.3 增加离线包热更新:让客户不用重装就能用新功能
客户部署在偏远矿区,网络不稳定。我们用plus.runtime.getProperty()获取当前版本号,配合plus.downloader.createDownload()下载 ZIP 离线包,解压后plus.runtime.restart()切换。
核心逻辑(js/update.js):
function checkUpdate() { plus.runtime.getProperty(function(widgetInfo) { const currentVer = widgetInfo.version; // 调用自己后端接口获取最新版本号和 ZIP URL fetch('/api/update?ver=' + currentVer) .then(r => r.json()) .then(data => { if (data.code === 0 && data.data.version > currentVer) { downloadAndInstall(data.data.url); } }); }); } function downloadAndInstall(url) { const dtask = plus.downloader.createDownload(url, {filename: '_doc/update.zip'}); dtask.addEventListener('statechanged', function(task, status) { if (task.state === 4 && status === 200) { // 下载完成,解压 plus.zip.unzip(task.filename, '_doc/update/', function() { // 解压完成,重启 runtime plus.runtime.restart(); }); } }); dtask.start(); }调用时机放在app.js的onPlusReady末尾,每次启动检查。ZIP 包内结构必须与www/一致(如www/js/app.js),解压到_doc/update/后,plus.runtime.restart()会自动从该路径加载。
从那以后我每次交付混合应用,都强制走一遍离线包流程——不是为了炫技,是给客户一颗确定性的心丸。希望帮到你。
本文还有配套的精品资源,点击获取