ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

uni-app三端同源实战:搭建58同城式生活服务App

uni-app三端同源实战:搭建58同城式生活服务App 简介面向需要快速搭建生活分类信息平台的开发者或创业者压缩包内是一套跨平台客户端源代码兼容 App、小程序、H5 等多个终端。整体业务对标 58 同城、赶集网围绕信息发布、置顶、刷新、佣金分成和广告位构建商业化闭环并内置社区论坛、房产、招聘、二手、拼车、生活服务等常用频道类目与信息属性支持自定义可覆盖多数行业场景。压缩包约 34.61MBzip 格式便于下载后直接解压查看工程源码、目录结构与多端适配方式适合已有前端或小程序基础的读者作为二次开发原型。目前已有 496 人浏览学习尤其适合需要理解分类信息平台权限控制、信息发布流程、付费置顶与佣金结算逻辑的开发者既能作为起步代码也可据此快速改造成自营同城服务项目。1. 一个生活服务App客户端为什么值得做成跨平台三端同源先看一个具体场景一个区域生活服务平台业务线覆盖房产、招聘、二手、家政、社区论坛过去靠PC站微信公众号两条腿走路。移动端需求一来老板要“App、小程序、H5三端都要有”而且要在三个月内跑通。如果安卓、iOS、微信小程序、H5各做一套原生或独立工程光排期就够喝一壶。这类项目的最佳方案就是用uni-app这类跨平台框架做一套Vue代码编译到App含iOS和Android、微信小程序、H5三端同时把分类信息、置顶、佣金、社区论坛、自定义栏目按模块化拆开。这篇就按一个一线工程师的落地路径把这个项目的工程骨架、数据表、业务链路和三端差异讲清楚。标题里“兼容58同城赶集网”不是在致敬UI而是指业务模型分类导航、信息列表、发布置顶、会员佣金、独立论坛、自定义栏目这套结构才是这类项目真正的核心。下面直接从工程搭建和数据设计开始。2. 跨平台工程怎么搭从uni-app目录结构到分类信息的后端表设计2.1 为什么选uni-app而不是Taro或Flutter做跨平台优先看三件事一套代码能到达的目的地、社区生态的成熟度、以及招聘市场上找到的人会不会用。uni-app基于Vue语法一个项目通过HBuilderX或CLI编译能产出微信小程序、H5、App打包成原生壳而且它在中国开发者里的普及程度远高于Taro。Flutter虽然性能和动画体验更好但Dart语法和UI写法与Web差异大团队如果纯Web背景学习成本会拉长交付周期。另外一点很实际这类生活服务项目通常需要大量表单页、列表页、地图选点和支付能力uni-app都提供了现成API。尤其“58同城赶集网”这类分类信息站最典型的页面结构——顶部搜索、分类宫格、信息流列表、底部TabBar——用uni-app的组件模型几乎可以一比一还原不需要像原生那样为每个平台单独维护一套UI。2.2 初始化工程先搭一个三端共用的页面骨架用HBuilderX可视化创建项目和用CLI创建项目都可行。工程化习惯更好的是CLI方式因为可以纳入Git版本管理也能对接现有的CI流程。命令如下# 使用vue3vite模板创建uni-app项目 npx degit dcloudio/uni-preset-vue#vite-ts life-service-client cd life-service-client npm install npm run dev:mp-weixin创建完项目后第一件事不是写业务而是梳理pages.json的全局配置。pages.json是uni-app的页面路由和原生导航配置文件三端公用。对一个分类信息平台来说底部TabBar通常有四个页首页分类导航、发布信息发布、社区论坛、我的个人中心。{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 生活服务 } }, { path: pages/release/release, style: { navigationBarTitleText: 发布信息 } }, { path: pages/community/community, style: { navigationBarTitleText: 社区论坛 } }, { path: pages/my/my, style: { navigationBarTitleText: 我的 } } ], tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/release/release, text: 发布 }, { pagePath: pages/community/community, text: 社区 }, { pagePath: pages/my/my, text: 我的 } ] } }这段配置决定了三端启动后看到的整体框架。tabBar的作用不只是UI层导航它还决定了App打包后原生底栏的样式以及小程序端的原生Tab切换体验。要注意图片资源路径问题小程序TabBar的iconPath在App端可以接受本地相对路径但在H5端需要确保图片能被正确打包否则会出现图标丢失。最省事的做法是先用文字TabBar跑通业务后续再添加图标。与之配套的是一个session管理模块和request封装。所有端共用axios不现实uni.request最适合。封装时把BASE_URL差异化处理开发环境直连局域网IP生产环境走线上域名。// utils/request.js const BASE_URL import.meta.env.VITE_API_BASE || https://api.example.com; export function request({ url, method GET, data {}, token true }) { return new Promise((resolve, reject) { uni.request({ url: ${BASE_URL}${url}, method, data, header: { Content-Type: application/json, Authorization: token ? uni.getStorageSync(token) : }, success: (res) { if (res.data.code 0) { resolve(res.data.data); } else if (res.data.code 401) { // 登录态失效跳转登录页 uni.navigateTo({ url: /pages/login/login }); reject(res.data); } else { reject(res.data); } }, fail: (err) reject(err) }); }); }这里的关键参数是import.meta.env.VITE_API_BASE因为不同端在请求域名上有限制小程序端要求域名在后台配置白名单且必须HTTPSH5端则没有这个限制但会面临跨域问题App端在Android上默认允许HTTP但iOS从ATS策略上会拦截明文请求。开发期会在manifest.json里临时勾选“iOS不校验https”上线前必须配置正式证书。2.3 分类信息平台的后端表设计category、info与置顶前端骨架只是壳真正撑起“58同城赶集网”这种分类信息模式的是数据库模型。核心有四张表栏目分类表lc_category、信息表lc_info、论坛表lc_forum_post、佣金流水表lc_commission_log。先看前两张的核心结构。CREATE TABLE lc_category ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 栏目名如房产/招聘/二手, parent_id int(11) NOT NULL DEFAULT 0 COMMENT 父级ID0为顶级分类, icon_url varchar(255) DEFAULT COMMENT 分类图标, form_config text COMMENT JSON发布表单的字段配置, sort int(11) NOT NULL DEFAULT 0 COMMENT 排序值越小越靠前, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1启用 0停用, PRIMARY KEY (id), KEY idx_parent_sort (parent_id, sort) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE lc_info ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id int(11) NOT NULL COMMENT 所属栏目, user_id bigint(20) NOT NULL COMMENT 发布者ID, title varchar(120) NOT NULL, content text COMMENT 详细描述, price decimal(10,2) DEFAULT 0.00 COMMENT 价格/租金等, images text COMMENT JSON数组最多9张图, address varchar(255) DEFAULT COMMENT 地址冗余展示用, lat decimal(10,7) DEFAULT 0 COMMENT 纬度, lng decimal(10,7) DEFAULT 0 COMMENT 经度, is_top tinyint(1) NOT NULL DEFAULT 0 COMMENT 0普通 1置顶, top_expire_at datetime DEFAULT NULL COMMENT 置顶到期时间, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1展示 0下架, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_category_status_time (category_id, status, create_time), KEY idx_user_time (user_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两张表配合使用首页分类导航从lc_category读取信息列表从lc_info按category_id分页查询。置顶信息很关键列表查询时ORDER BY is_top DESC, create_time DESC即置顶记录优先展示同级别按发布时间倒序。这里藏着一个性能优化点如果置顶记录多全表排序还是会导致扫描范围变大正确做法是把置顶单独用Redis缓存一个列表普通信息走MySQL分页两边合并后返回。不过初期数据量不大单表同样可以跑得很顺。栏目表里的form_config字段是整个“自定义栏目”能力的来源。它存一个JSON数组定义了发布信息时这个栏目需要填写哪些字段比如房产要填面积、朝向、户型二手要填成色、原价、转让价。不同栏目用不同的动态表单而不是为每个栏目单独写前端页面这就是“自定义栏目”的核心落地方式。下一章把这条链路完整串起来。3. 信息发布、置顶与佣金核心业务链路的完整实现3.1 动态表单用form_config配置驱动发布页发布页的逻辑是先选一级栏目再选二级栏目确定category_id后向后端请求栏目的form_config前端根据JSON渲染表单。form_config结构设计如下[ { key: title, label: 标题, type: input, required: true, maxlength: 50 }, { key: price, label: 价格, type: number, required: false }, { key: area, label: 面积, type: input, required: true, placeholder: 输入建筑面积 }, { key: orientation, label: 朝向, type: picker, options: [东, 南, 西, 北, 南北] } ]前端用一个循环渲染所有配置项template view v-forfield in formConfig :keyfield.key input v-iffield.type input || field.type number v-modelformData[field.key] :placeholderfield.placeholder || 请输入 field.label / picker v-else-iffield.type picker :rangefield.options changeonPickerChange(field, $event) view{{ formData[field.key] || 请选择 field.label }}/view /picker /view /template提交前校验所有required字段把formData连同category_id以及后续要讲的位置、图片一起POST到/api/info/create。校验逻辑要和后端校验规则保持完全一致否则会出现H5能提交、小程序被后端拒绝的奇怪问题。动态表单的价值不只是省代码它让运营同学在后台新增栏目、调整字段时不需要重新发版。这是这类项目比纯静态页面更“长久”的原因。3.2 图片上传和定位三端兼得的API怎么用信息发布离不开图片生活服务里房子的户型图、二手物品实物图都是刚需。uni-app提供了uni.chooseImage和一个关键APIuni.compressImage。原生端和Web端的文件对象差异很大统一的做法是先选图、然后用tempFilePaths里的路径直接上传而不是在前端做过多兼容。async function uploadImages() { const res await uni.chooseImage({ count: 9, sizeType: [compressed] }); const uploadTasks res.tempFilePaths.map((filePath) { return new Promise((resolve, reject) { uni.uploadFile({ url: ${BASE_URL}/api/upload, filePath, name: file, success: (res) resolve(JSON.parse(res.data)), fail: (err) reject(err) }); }); }); const results await Promise.all(uploadTasks); that.formData.images results.map((r) r.data.url); }sizeType: [compressed]很关键。App端和美团的拍照场景不同生活服务用户大多是随手拍原图可能5MB以上压缩后能显著减少上传时间和服务器带宽消耗。后端收到文件后要再次校验扩展名和大小我在实际项目中遇到过快图秀秀生成的图片伪造扩展名导致安全问题的情况。定位用uni.getLocation。注意三端差异微信小程序需要用户授权且manifest.json里声明requiredPrivateInfos中的getLocationApp端需要在原生打包时申请定位权限H5端则走浏览器Geolocation。如果H5端用户拒绝授权定位失败时应该降级为手动输入地址而不是直接阻断发帖流程。3.3 置顶逻辑到期自动下架是怎么实现的置顶是这类平台的核心变现手段。用户发布信息后是普通状态付费购买置顶服务后is_top变为1同时写入top_expire_at。前端购买置顶后调用支付接口支付回调里更新这两项字段。UPDATE lc_info SET is_top 1, top_expire_at DATE_ADD(NOW(), INTERVAL #{days} DAY) WHERE id #{infoId}有人会问到期后的自动下架要不要写定时任务看数据量。信息量少时用定时任务扫描也可以但更稳的做法是在列表查询时动态判断WHERE is_top 1 AND top_expire_at NOW()。这个条件有两个效果一是过滤掉已过期的置顶记录二是让它们自然降级为普通信息不需要额外更新操作。问题在于is_top字段此时语义不对——它表示“购买过置顶服务”而不代表“当前处于置顶状态”。我习惯保留这个字段做统计而在查询状态时需要加时间条件去判断当前是否生效这和标题里“二手房源到期自动落下”的逻辑是同一套。3.4 佣金结算从订单到分账流水佣金机制在标题里和“置顶”并列指的是平台撮合交易后对商家收取的服务费比例。业务模型是商家发布服务时设置佣金比例用户通过平台下单后订单金额冻结确认服务完成后按比例自动分账。数据库里建一张commission_log表CREATE TABLE lc_commission_log ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id varchar(64) NOT NULL COMMENT 订单号, info_id bigint(20) NOT NULL, from_user_id bigint(20) NOT NULL COMMENT 买家/服务方, to_user_id bigint(20) NOT NULL COMMENT 导流对象或服务方, amount decimal(10,2) NOT NULL COMMENT 订单金额, commission_rate decimal(5,2) NOT NULL DEFAULT 0 COMMENT 佣金比例, commission_amount decimal(10,2) NOT NULL COMMENT 佣金金额, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待结算 1已结算 2已退款, create_time datetime NOT NULL, settle_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;结算时点不是一个绝对标准常见做法是“确认收货后T1自动结算”。把佣金计算从订单流程里独立出来不要在用户下单时直接改余额而是先生成待结算流水等定时任务或事件触发后再更新为已结算。这样做的好处是退款或售后发生时直接标记2已退款不会出现资金先出去又追回来的尴尬。4. 社区论坛与自定义栏目组件化思路下的跨端适配4.1 帖子列表、回复、点赞的跨端组件封装论坛模块在这个项目里和第二方分类信息不同它的互动性更强。列表页要做三件事分页加载、下拉刷新、滚动位置保持。uni-app中分页写在onReachBottom页面生命周期里它与Vue的mounted不同在不同端触发机制有差异但大多数情况下行为一致。一个帖子列表卡片往往是复用度最高的组件也最容易出现样式漂移。小程序端盒子模型渲染与H5的Web引擎差异导致了同一个padding: 10rpx在两端产生几像素的偏差。template view classpost-card clickgoDetail(post.id) view classpost-header text classnickname{{ post.nickname }}/text text classtime{{ formatTime(post.create_time) }}/text /view view classpost-title{{ post.title }}/view view classpost-content rich-text-content rich-text :nodespost.content/rich-text /view view classpost-actions view classaction-item click.stoplikePost(post.id) text{{ post.likeCount }} 赞/text /view view classaction-item click.stoptoggleReply(post.id) text回复/text /view /view /view /template富文本是论坛最需要小心的地方。网络热词里反复出现“h5游戏逆向”“尝试新的跨平台powershell”之类的内容和论坛直接相关的是很多用户发帖时从第三方复制粘贴内容HTML的标签在小程序端不能用v-html渲染要么用rich-text组件要么用后端过滤后的JSON节点。rich-text支持常用标签但不支持事件绑定和视频播放如果要发视频帖子小程序端用video组件H5端集成video.js这是组件化时要提前拆分的版块。点赞和收藏用防重复请求function likePost(postId) { if (likeLoading) return; likeLoading true; request({ url: /api/forum/like, method: POST, data: { postId } }) .then(() { uni.showToast({ title: 感谢点赞, icon: success }); }) .finally(() { likeLoading false; }); }4.2 自定义栏目运营后台配置前端动态渲染“自定义栏目”应该是这个标题里最好实现也最容易做差的一块。实现方式是建立column表包含column_name、column_type、bind_category_id、fields_config字段。以二手交易为例运营在后台配置“闲置转让”栏目的字段集合发布页通过fields_config生成表单列表页根据list_fields决定展示哪些列。一个实用参数是sort_mode置顶优先、最新优先、价格升序、价格降序。这个排序参数直接决定列表页的查询语句拼接前后端要统一定义枚举值我见过前端传price_asc、后端判断priceAsc导致排序不生效的线上事故。当栏目数量超过20个时前端不可能为每个栏目写独立模块。统一用“栏目配置JSON 通用列表组件”的方案后期新增“拼车”、“宠物”之类栏目运营后台配置完即可上线这个模式就是标题里“自定义栏目等”的实际落地。4.3 H5、小程序、App的三端运行时差异清单这是这个项目最容易翻车的地方提前列一张差异清单能力微信小程序H5App登录态wx.login code换tokenJWT localStorage原生SDK/JWT本地存储uni.setStorageSync 走微信存储localStorageSQLite/文件定位需在manifest声明需用户授权HTTPS原生权限弹窗支付微信支付微信H5支付/支付宝原生App支付页面刷新没有原生刷新机制浏览器刷新即重载原生生命周期富文本rich-text组件v-htmlrich-textApp端是webview渲染支持度不同登录态差异影响最大。小程序里uni.setStorageSync存的token在App端也能用但App端如果同时打包了iOS和Android两个端的存储是隔离的。H5端用户浏览器清缓存会导致token丢失App端则相对稳定。建议所有端都用自己的机制不要试图把token同步到localStorage和uni.setStorageSync两套方案里去。论坛里的“房产招聘二手”这三个核心栏目往往被设计成自定义栏目的预置实例而不是单独写死页面。列表页和详情页做成一个通用页面通过路由参数categoryId区分加载哪一类数据配不同的form_config和列表字段这是在工程上“兼容58同城赶集网”最经济的方式。5. 三端联调与发布几个必须提前避开的坑先说调试。H5端最轻松浏览器DevTools直接断点。小程序端用微信开发者工具但经常遇到“当前不会命中断点”的提示。这个问题的多数原因是代码经过了压缩混淆或sourcemap没有正确关联。解决方法开发者工具里把“ES6转ES5”暂时关掉编译模式选“开发版”如果还不行就清理编译缓存重新编译。另一种情况是想调试onLaunch里的逻辑但断点打在回调函数内部异步代码的栈已经被回收这个断点自然命中不了。App端调试用HBuilderX内置的“运行到手机或模拟器”。要注意真机调试时手机和电脑必须连同一个局域网否则HBuilderX会卡在“正在同步文件”阶段。App端的网络请求如果出现“网络请求失败”十有八九是域名没有在manifest.json的“App SDK配置”里声明或没有配置合法域名。H5端跨域则在服务端配置CORSAccess-Control-Allow-Origin: 你的域名。小程序端有个更隐蔽的坑域名必须在微信公众平台的“开发管理-服务器域名”里加白名单否则request请求直接fail在真机上需要刷新缓存才生效。开发期可以在开发者工具右上角勾选“不校验合法域名”但真机预览会强制校验。发布流程上H5打包到服务器后微信内访问如果要用定位需要单独接入微信公众号JS-SDK用wx.getLocation而不是uni.getLocation。这里有一个常见错误H5页面在微信内置浏览器中直接调用HTML5 Geolocation会弹出许可但只有部分安卓机型和微信版本支持低版本会静默失败。封装一个带降级策略的位置获取方法function getH5Location() { return new Promise((resolve, reject) { if (isWeChatBrowser()) { // 通过后台接口注入wx.config然后调用wx.getLocation wx.ready(() { wx.getLocation({ type: gcj02, success: (res) resolve({ lat: res.latitude, lng: res.longitude }), fail: reject }); }); } else { uni.getLocation({ success: resolve, fail: reject }); } }); }发布App时iOS和Android的证书、包名、图标、启动图都必须在打包前准备好。iOS还涉及Apple开发者账号的Bundle Identifier和证书Profile的匹配问题任何一个不匹配都会被App Store Connect拒绝。常见做法是HBuilderX云打包生成ipa或apk但这需要DCloud账号的打包次数团队内要提前规划。最后一个技巧三端共用一套接口但线上环境建议给不同端的请求头加一个X-Client-Type: mp-weixin / h5 / app。后端可以根据端类型返回不同粒度数据比如小程序端图片尺寸压缩、H5端返回完整HTML富文本、App端返回JSONNode。这个头能帮助你快速排查线上问题——最怕的是用户说“App打不开某个页面”结果你复现不出来最后发现是H5入口转发到了App没覆盖到的路由。整个项目的开发顺序建议是先做H5端跑通全部业务逻辑因为调试成本最低再编译到小程序端适配差异最后打包App壳——这样能把跨端问题拆分到不同阶段避免三端同步瞎忙。本文还有配套的精品资源点击获取
返回列表