尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

uni-app+Vue+ThinkPHP校园极简App多端WebSocket实战

uni-app+Vue+ThinkPHP校园极简App多端WebSocket实战 简介这份PDF文献面向计算机相关专业学生、移动应用开发初学者及毕业设计选题者围绕uni-app跨平台框架在校园场景中的落地展开。全文系统梳理了跨平台移动应用开发的概念与意义介绍uni-app可发布至Android、iOS、H5及微信、支付宝、头条、百度等多端小程序的特性并与Flutter、React Native在技术路线、学习成本与生态资源上做了对比分析。文中还阐述了校园极简生活的内涵并给出系统的框架结构设计与功能模块设计涵盖登录注册、分类浏览、信息检索、信息发布、关注互动与个人管理其中分类细分为物品交换、时间管理、学习充电、健康环保、心灵减压五个主题。资源包为单一PDF文件体积约1.43MB适合作为课程设计、论文写作与项目立项的参考材料已有1424人学习下载。1. 一套代码覆盖五个端的取舍校园极简生活应用的 uni-app 起点校园里同时存在 iPhone、安卓机、只装了微信的平板以及只在浏览器里逛的同学——这个真实的设备分布逼着任何一个三人规模的学生项目必须一开始就回答“要不要写原生”。苏州市职业大学这个项目选了最省人的路线用 uni-app 写一套 Vue 代码打包成 iOS、Android、微信小程序和 H5围绕物品交换、时间管理、学习充电、健康环保、心灵减压五个主题做一个鼓励极简生活方式的校园社区。它适合第一次碰跨平台开发、又不想先啃 Dart 或 React 的人也适合已经会 Vue、打算把已有项目推到多端的前端。下面按工程骨架、数据流、长连接、打包部署四条线把这条实现路径拆开讲重点说清哪些坑是必须提前避开的。2. uni-app 工程骨架与 pages.json 路由五类主题模块的组织方式跨平台框架的选型往往不是性能之争而是学习曲线之争。这个项目把 Flutter、React Native、uni-app 放在同一张表里对比后才落笔结论是要学习成本最低的那条路。维度FlutterReact Nativeuni-app开发语言DartJavaScript/JSXVue渲染方式自绘 UIJS 桥接原生组件混合渲染WebView 原生是否需原生协作需要需要基本不需要可发布端Android、iOSAndroid、iOSAndroid、iOS、H5、多套小程序上手门槛高中低社区与插件丰富丰富国内插件市场活跃Dart 和 React 的前置知识是实打实的成本而 uni-app 只要求会写 Vue这一点对课程项目和大三实习前的练手极关键。2.1 pages.json 里定义页面与分包uni-app 的页面路由不是写在 Vue Router 里而是统一配置在pages.json。登录注册、分类浏览、信息检索、信息发布、关注互动、个人管理这六个模块映射成页面后大致是这样{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 极简校园 } }, { path: pages/login/login, style: { navigationBarTitleText: 登录注册 } }, { path: pages/topic/list, style: { navigationBarTitleText: 分类浏览, enablePullDownRefresh: true } }, { path: pages/search/search, style: { navigationBarTitleText: 信息检索 } }, { path: pages/publish/publish, style: { navigationBarTitleText: 发布信息 } }, { path: pages/chat/chat, style: { navigationBarTitleText: 私信 } } ], tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/topic/list, text: 分类 }, { pagePath: pages/publish/publish, text: 发布 }, { pagePath: pages/mine/mine, text: 我的 } ] } }pages数组的第一项就是启动页顺序不能随意调tabBar里只放四个高频入口其余页面用uni.navigateTo跳转。enablePullDownRefresh打开后要在页面里实现onPullDownRefresh否则下拉只是空转。2.2 五个主题分类的落地方式物品交换、时间管理、学习充电、健康环保、心灵减压这五个分类不适合写死成五套页面而是用同一个列表页加categoryId参数区分// pages/topic/list.vue 里根据路由参数拉取不同分类数据 onLoad(options) { this.categoryId options.id || 1; this.fetchList(); }, methods: { async fetchList() { // baseURL 指向 ThinkPHP 控制器暴露的接口 const res await uni.request({ url: this.$baseUrl /api/topic/list, data: { category_id: this.categoryId, page: this.page } }); if (res.data.code 0) { this.list this.list.concat(res.data.data); } } }onLoad只在页面首次加载时触发一次适合拿路由参数onShow每次切回都会跑适合刷新点赞数这类易变状态。这个区分如果搞混会出现“从详情页返回后点赞数不更新”的典型 bug。2.3 HBuilderX 工程目录的约定项目用 HBuilderX 创建目录结构遵循约定优于配置pages/放页面目录名与pages.json中的path一一对应static/放图片与字体小程序端对包体大小敏感超过 2MB 的图片要挪到 CDNcomponents/放自定义组件比如点赞按钮、统一的信息卡片common/放request.js、config.js这类公共模块提示static目录下的资源在小程序端会被整体打进主包随手丢进去的几张 3MB 大图很可能就是发布失败的直接原因。3. MVVM 双向绑定与 ThinkPHP 接口约定前后端分离的数据流拆解前端用 MVVM后端用 MVC两套模式在同一请求链路上各管一段。前端关注的是 View 与 Model 的分离后端关注的是模型、视图、控制器的分层。3.1 MVVM 在 uni-app 里的对应关系在 Vue 实例里View 对应模板所控制的 DOM 区域ViewModel 就是 Vue 实例本身Model 就是data里的值。三者之间靠双向绑定打通export default { data() { return { keyword: , // 对应检索框的 v-model resultList: [] // 渲染到列表的 Model }; }, methods: { async doSearch() { if (!this.keyword.trim()) return; const res await uni.request({ url: this.$baseUrl /api/search/index, data: { kw: this.keyword.trim(), page: 1 } }); // ViewModel 改 dataView 自动重渲染无需手动操作 DOM this.resultList res.data.data || []; } } };v-model只负责把输入框的值同步进keyword真正的检索要显式调用doSearch。刚上手时容易误以为v-model会自己发请求结果输入沉重地敲了半屏字列表还是一动不动。3.2 后端控制器与接口返回格式后端用 ThinkPHP 5.1控制器方法对应一个接口。前端的每一次uni.request最终都落到某个controller/action上?php namespace app\api\controller; use think\Controller; use think\Db; class Topic extends Controller { // GET /api/topic/list?category_id1page1 public function list() { $categoryId input(category_id/d, 1); // d 修饰符强制转整型防注入 $page input(page/d, 1); $data Db::name(topic) -where(category_id, $categoryId) -where(status, 1) -page($page, 10) -order(create_time desc) -select(); // 统一返回结构前端按 code 判断成败 return json([code 0, msg ok, data $data]); } }input(category_id/d)里的/d是关键它把参数强制转成整型避免 SQL 注入和类型不一致导致的查询异常。返回结构里code是业务码而不是 HTTP 状态码0 表示成功前端统一在request.js里拦截处理。3.3 请求拦截与统一错误处理每处都写一遍if (res.data.code ! 0)会传染式地拖垮代码。做法是封装request.js// common/request.js const BASE_URL https://your-domain.com; export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { content-type: application/json, token: uni.getStorageSync(token) || }, success: (res) { if (res.data.code 0) { resolve(res.data); } else if (res.data.code 401) { // 登录态失效跳登录页 uni.reLaunch({ url: /pages/login/login }); reject(res.data); } else { uni.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }token从本地存储里取越权接口一律返回 401前端集中跳登录。这样业务页面里只需要await request({...})成功分支直接拿数据失败分支由封装层统一兜底。注意uni.getStorageSync在小程序端是同步 API高频调用会阻塞渲染。不到必要场景别在onPageScroll里反复读 token。3.4 表结构里的关键字段约定分类与信息之间的映射直接决定了查询效率主表结构大致如下字段名类型说明idint unsigned主键自增category_idtinyint1 物品交换 2 时间管理 3 学习充电 4 健康环保 5 心灵减压user_idint unsigned发布者contenttext正文图片以 URL 形式追加statustinyint1 正常 0 下架create_timeint时间戳category_id上要单独建索引否则五个分类的列表页在数据量过万后查询会明显变慢。4. GatewayWorker 长连接与 WebSocket 私信链路TCP 推送怎么落私信是这套系统里唯一需要服务端主动推送的功能也是 PHP 项目里最容易做歪的一环。传统Ajax轮询要么延迟高要么白白耗服务器项目最终落到 GatewayWorker 上。4.1 为什么不让客户端直连 Gateway客户端确实能用uni.connectSocket直连 Gateway但这意味着客户端要自己处理鉴权、消息路由、在线状态维护这一堆业务逻辑。项目的做法是让客户端只走 HTTP 到 ThinkPHP 控制器由控制器把消息转交 GatewayWorkerGateway 只当成一条单向推送通道。好处是业务逻辑集中在一处客户端换个端也不用改协议代价是消息链路多一跳但对校园场景完全够用。// controller/Chat.php use GatewayClient\Gateway; class Chat extends Controller { public function send() { $fromUid $this-checkToken(); // 从 token 解析出当前用户 $toUid input(to_uid/d); $content trim(input(content, )); if (!$toUid || $content ) { return json([code 1, msg 参数错误]); } // 消息先落库保证离线用户也能拉到 Db::name(message)-insert([ from_uid $fromUid, to_uid $toUid, content $content, is_read 0, create_time time() ]); // uid 与 client_id 的绑定关系在用户上线时写入 $toClientId Gateway::getClientIdByUid($toUid); if ($toClientId) { Gateway::sendToClient($toClientId[0], json_encode([ type chat, from $fromUid, content $content ])); } return json([code 0, msg 已发送]); } }关键点有三个消息必须先落库再推送离线用户下次上线能拉到Gateway::getClientIdByUid查绑定关系未上线时返回空数组不能直接取下标sendToClient只负责推不负责业务别把逻辑写在这里。4.2 客户端连接与心跳前端在登录成功后建立连接并定时发心跳保活// common/socket.js let socketTask null; export function connectSocket(uid) { socketTask uni.connectSocket({ url: wss://your-domain.com/wss, success: () console.log(socket 已连接) }); socketTask.onOpen(() { // 绑定 uid服务端据此建立 uid - client_id 映射 socketTask.send({ data: JSON.stringify({ type: bind, uid }) }); startHeartbeat(); }); socketTask.onMessage((res) { const msg JSON.parse(res.data); if (msg.type chat) { // 交给页面处理推荐用 uni.$emit 全局广播 uni.$emit(newMessage, msg); } }); socketTask.onClose(() { stopHeartbeat(); setTimeout(() connectSocket(uid), 3000); // 断线重连 }); } let timer null; function startHeartbeat() { timer setInterval(() { socketTask socketTask.send({ data: JSON.stringify({ type: ping }) }); }, 30000); // 30 秒一次小于网关空闲超时 } function stopHeartbeat() { clearInterval(timer); }bind消息必须在上线后立即发否则getClientIdByUid永远查不到心跳间隔要小于服务端空闲超时通常在 30 秒左右太长会被中间层掐断连接。断线重连用固定 3 秒而不是指数退避是因为校园网络抖动多为瞬时快速重连体验更好。4.3 好友、黑名单与消息过滤关注列表和黑名单在推送前做一次过滤// 推送前先判断对方是否被拉黑 $blocked Db::name(user_relation) -where(user_id, $toUid) -where(target_id, $fromUid) -where(is_block, 1) -find(); if ($blocked) { return json([code 0, msg 已发送]); // 静默丢弃不提示发送方 }静默丢弃是刻意的选择如果直接返回“已被拉黑”等于把黑名单关系暴露给了发送方。场景处理方式好友且在线落库 实时推送好友但离线只落库上线后拉取被对方拉黑落库但不推送不提示陌生人主动发起允许未被拉黑即可4.4 常见故障定位连不上时先查三处Nginx 是否为/wss配了 WebSocket 升级头防火墙是否放行了 Gateway 的注册端口和网关端口Gateway::$registerAddress是否与 GatewayWorker 启动配置一致。链路每多一个配置项就多一个出错点排查时按“客户端→Nginx→Gateway→ThinkPHP”顺序逐步隔离不要一上来就改业务代码。5. 多端打包差异与 Nginx 部署的排错清单一套 UI 跑在五个端视图差异是必然会冒出来的。H5 用的是浏览器渲染小程序有自己的一套组件限制iOS 和 Android 起的是原生壳CSS 在处理圆角、阴影、position: fixed时表现各异。实用的做法是把差异集中到少量公共样式里用条件编译区分/* 按照平台条件编译避免样式散落各处 */ .chat-input { /* #ifdef H5 */ padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); /* #endif */ /* #ifdef MP-WEIXIN */ bottom: 0; /* 小程序端由系统自带安全区处理 */ /* #endif */ }#ifdef和#endif之间的代码只会在对应平台编译进去H5 端适配刘海屏、小程序端交给系统不用写两套文件。打包时把端和目标分清楚。Android 侧输出 apk 或 aabiOS 侧输出 ipa需要开发者账号打包本身在 HBuilderX 云打包或本地 Xcode 里完成微信小程序输出的是项目源码包交给微信开发者工具上传。后端部署在阿里云 CentOS 上用宝塔面板装 Nginx 和 MySQL。Nginx 两块配置最容易出错server { listen 443 ssl; server_name your-domain.com; # 1. H5 前端静态资源 root /www/wwwroot/campus-h5; index index.html; # 2. ThinkPHP 后端接口 location /api/ { root /www/wwwroot/campus-api/public; fastcgi_pass unix:/tmp/php-cgi-73.sock; fastcgi_param SCRIPT_FILENAME $document_root/index.php; include fastcgi_params; } # 3. WebSocket 升级少了 Upgrade 和 Connection 这两行必定握手失败 location /wss { proxy_pass http://127.0.0.1:8282; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header X-Real-IP $remote_addr; } # 4. 前端 history 路由回退否则刷新页面直接 404 location / { try_files $uri $uri/ /index.html; } }几个容易忽略的点proxy_http_version 1.1不写WebSocket 一定失败try_files不写H5 刷新内页就是白屏fastcgi_param SCRIPT_FILENAME指向index.php而不是$document_root$fastcgi_script_name是因为入口文件在public下路径层级要对齐。验证部署是否真的通直接命令行打过去最快# 探接口 curl -H Content-Type: application/json \ https://your-domain.com/api/topic/list?category_id2page1 # 探 WebSocket 握手返回 101 才算升级成功 curl -i -N \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Version: 13 \ -H Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ \ https://your-domain.com/wss接口返回{code:0,...}说明 PHP 侧通了握手返回101 Switching Protocols说明 Nginx 到 GatewayWorker 这一跳没问题。两边都通但客户端仍收不到消息大概率是bind没发出去或者uid与client_id的映射在用户重连后被清了——改重连逻辑时记得在onOpen里补一次bind。本文还有配套的精品资源点击获取
返回列表