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

资讯详情

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

ThinkPHP+Uniapp社区朋友圈源码深度拆解与二次开发指南

ThinkPHP+Uniapp社区朋友圈源码深度拆解与二次开发指南 简介基于 ThinkPHP5.1、Uniapp 与 MySQL5.7 开发的社区朋友圈源码主要兼容 H5 端后端采用 MVC 设计模式前端基于 Uniapp 并使用 uview-UI 框架图片压缩接入七牛云存储。这套源码面向想学习社区类程序开发的 PHP 与前端新手也适合计划二次开发的读者代码注释量大可参考登录注册、动态发布、图片上传、接口路由等模块的实现思路并借此扩展跑腿、支付等业务功能。资源包共 2000 个文件、体积约 9.47MBJS 与 PHP 文件占据主体分别对应前端交互与后端接口逻辑另有 Vue 组件、JSON 配置、WXML/WXSS 小程序页面、图片与 GIF 素材等覆盖页面样式、数据配置及多端适配资源。目前已有 543 人浏览学习。需要说明的是作者实测该源码仅适合学习研究无法直接商用部署读者下载前请确认是否接受这一限制。1. 拿到社区朋友圈源码先拆的是 feed 流而不是界面拿到这套 ThinkPHP 5.1 Uniapp 的社区朋友圈源码时我先拆的不是动态发布和点赞按钮而是把数据库表结构全部导出来看了一遍。原因很简单朋友圈这类时间线产品的代码量并不大真正的门槛在 feed 流的查询模型设计以及图片从上传、压缩到 CDN 回源的整条链路能不能扛住并发。这套项目恰好把这两块用最直白的方式摆在一个可运行的 demo 工程里后端走 MVC 分层前端用 Uniapp 搭配 uview-UI 组件库数据库跑在 MySQL 5.7主要兼容 H5 端。作者在源码里保留了非常多的注释适合用来理解一个社区类程序从请求到响应、再到外部对象存储的完整闭环在这个基础上改出跑腿、支付这类带业务状态的模块也比从零起框架快得多。需要先说明的是这套代码是学习样本而不是商业成品直接部署上线大概率会在鉴权、内容过滤、接口频率限制这些环节暴露出问题后面我会把每一处坑的位置标出来。2. 项目目录结构与 MVC 分层落点2.1 后端 ThinkPHP 5.1 的目录映射关系ThinkPHP 5.1 的 MVC 不是概念性的它直接反映在application目录结构上。这套源码把index模块作为前端接口的唯一入口控制器、模型、视图三个目录各司其职。我一般拿到 TP 项目会先看application/index/controller下面有哪些文件因为控制器的命名直接对应路由前缀比如Dynamic.php对应/index/dynamic这个 URLUniapp 端请求的接口路径就是从这儿来的。application/ ├── index/ │ ├── controller/ │ │ ├── Dynamic.php # 动态发布/列表/删除 │ │ ├── Comment.php # 评论与回复 │ │ ├── User.php # 用户注册/登录/资料 │ │ └── Upload.php # 七牛 token 签发 │ ├── model/ │ │ ├── Dynamic.php │ │ ├── User.php │ │ └── Comment.php │ └── view/ # 管理端模板 ├── common.php # 公共函数 └── config/ # 应用配置前端走https://域名/index.php?s/index/dynamic/lists这类入口文件格式实际上 TP 5.1 默认支持 pathinfo 模式只要在 Nginx 里配置好try_files就能去掉index.php。注意这里有个兼容点Uniapp 编译到 H5 后请求的 baseURL 如果写成相对路径在微信公众号里打开时会被当前页面路径覆盖建议直接配置成完整域名加/index.php哪怕不上伪静态也不会影响功能。2.2 前端 Uniapp 工程与 uview-UI 的接入方式前端项目在src或根目录下是一个标准 Uniapp 工程页面集中在pages目录公共组件和工具函数放在common与utils。uview-UI 的接入方式是典型的main.js全局挂载这套源码用的还是 uview 1.x 版本和 Vue 2 的 Uniapp 项目匹配如果强行升级 Vue 3 需要换 uview-plus这个话题我放到最后一章展开。import uView from /uview-ui Vue.use(uView) // 全局请求封装统一处理 token 与错误码 uni.$u.http.setConfig({ baseURL: https://yourdomain.com/index.php, withCredentials: true }) uni.$u.http.interceptors.request.use(config { const token uni.getStorageSync(token) if (token) config.header.Authorization token return config })这段代码的关键在两个地方withCredentials: true是为了让 H5 端跨域请求带上 Cookie因为 TP 5.1 的 session 认证依赖 Cookieuniapp 打包成 App 时这个配置不生效但 H5 必须开拦截器里手动附加 token 是为了后续改成 JWT 认证预留位置。源码里大量使用uni.$u.http而不是直接uni.request说明作者把 uview 的请求封装当作了统一出口二开时新增接口只需要按同样的模式扩展。2.3 核心数据表设计与字段含义朋友圈业务的数据表不多但每张表的字段都有讲究。我整理了一份核心表清单顺序按照从主到从排列表名主要字段作用说明userid, nickname, avatar, status用户基础信息status 字段控制封禁状态dynamicid, user_id, content, images, location, create_time动态主表images 存 JSON 数组commentid, dynamic_id, user_id, reply_id, content评论表reply_id 支持楼中楼like_logid, dynamic_id, user_id, create_time点赞记录表唯一索引防止重复点赞user_followid, follower_id, following_id关注关系表默认不启用其中dynamic.images存 JSON 而不是单独建关联表这是一个值得讨论的选型。单独建dynamic_image表看起来更范式化但查询时多一次 join而且图片数量固定为九宫格上限JSON 字段配合json_decode完全够用还能直接存进七牛返回的完整 URL 列表。缺点是统计图片数量需要count()函数在 PHP 层做不能直接走 SQL数据量上来后这是一个不可忽视的代价。2.4 MySQL 5.7 环境与 TP 5.1 配置文件的对应关系源码的数据库配置在application/database.phpTP 5.1 支持在.env文件中覆盖部署时推荐用.env而不是直接改代码里的配置文件避免把测试库连接串提交到 Git。[database] hostname 127.0.0.1 database community username root password yourpass hostport 3306 charset utf8mb4 [七牛] access_key your_ak secret_key your_sk bucket your_bucket domain https://img.yourdomain.com这里注意charset utf8mb4必须显式声明如果数据库表本身是 utf8mb4 而连接字符集是 utf8用户在朋友圈输入 emoji 时会把\xF0\x9F\x98\x80截断成乱码甚至直接报 SQL 错误——这是社区类项目里最常见的字符集事故。另外 MySQL 5.7 默认开启了ONLY_FULL_GROUP_BY如果二开时写了GROUP BY user_id但 SELECT 里带了*会直接抛 1055 错误遇到这种情况先检查 sql_mode 而不是怀疑 SQL 语法。3. 朋友圈时间线的查询模型与 TP 5.1 模型层实战3.1 feed 流的核心难点是数据组装而不是查询朋友圈时间线从产品形态上叫 feed 流但在 MySQL 5.7 这个单库场景下根本碰不到复杂排序算法最实际的做法就是dynamic表按create_time倒序分页再批量补上用户信息和点赞状态。这里有一个新手容易踩的坑在循环里写User::get($item[user_id])会导致 N1 查询动态 20 条就是 21 条 SQL后期接口响应时间直接线性恶化。正确做法是先取出user_id数组用一次whereIn查出所有用户信息再在内存里组装成映射表。TP 5.1 的模型提供with预加载来屏蔽这个过程但源码里用的是查询构造器配合数组映射更直观也更容易调优。我给出的实现是集合式处理方案public function lists($page 1, $limit 10) { // 分页参数约束防止负数或超大值打到数据库 $page max(1, intval($page)); $limit min(50, max(1, intval($limit))); // 动态主表数据只需一次查询 $list Db::name(dynamic) -where(status, 1) -order(create_time, desc) -page($page, $limit) -select() -toArray(); if (empty($list)) { return []; } // 取出动态 id 和用户 id准备批量查询 $dynamicIds array_column($list, id); $userIds array_unique(array_column($list, user_id)); // 批量查用户信息避免 N1 $users Db::name(user) -whereIn(id, $userIds) -column(nickname,avatar, id); // 批量统计每条动态的评论与点赞数 $commentCount Db::name(comment) -whereIn(dynamic_id, $dynamicIds) -group(dynamic_id) -column(COUNT(*), dynamic_id); $likeCount Db::name(like_log) -whereIn(dynamic_id, $dynamicIds) -group(dynamic_id) -column(COUNT(*), dynamic_id); foreach ($list as $item) { $item[user] $users[$item[user_id]] ?? []; $item[images] json_decode($item[images], true) ?: []; $item[comment_count] $commentCount[$item[id]] ?? 0; $item[like_count] $likeCount[$item[id]] ?? 0; } return $list; }这段代码里whereIn加column是关键组合column的第二个参数指定用id作为数组的 key这样后面$users[$item[user_id]]的映射就是 O(1) 的数组索引。group(dynamic_id)配合column(COUNT(*), dynamic_id)返回的是一个[dynamic_id count]的关联数组而不是二维表结构这一步节省了 PHP 层的一次循环遍历。3.2 分页参数设计与下拉刷新的配合方式Uniapp 端朋友圈最常见的是 onPullDownRefresh 配合触底加载对应到接口分页参数一般是page和limit也可以叫start和count。源码采用的是前者我认为这个选型对了因为 TP 5.1 的page($page, $limit)方法直接就是页码与条数的语义不需要在 SQL 里手动算 offset。前端请求时的参数校验必须做在服务端不能信任limit传来的 9999 这种值。上面代码里的min(50, max(1, intval($limit)))就是一道保险把单页条数上限硬性压到 50。实战中还有一个细节下拉刷新应该重置page1并清空列表数组触底加载则page1并追加列表如果后端没有在返回包里带上has_more字段前端就得靠本次返回条数 limit来判断是否到底这套源码就是后者二开时增加一个has_more字段会更稳。3.3 动态删除与关联数据的清理删除一条动态不只是删dynamic表那一行还要带走它下面的评论、点赞记录以及七牛上的图片原图。如果在删除时遗漏关联清理用户会看到一个动态已消失但评论数和点赞数残留在统计接口里这些脏数据会被后台统计报表计算进去误导运营决策。TP 5.1 的模型关联删除是一种常见做法用hasMany定义好关联后写在删除逻辑里public function deleteDynamic($dynamicId) { // 事务保证多表删除的一致性 Db::startTrans(); try { // 查出 images 字段删除七牛 OSS 文件 $dynamic Db::name(dynamic)-where(id, $dynamicId)-find(); if (!empty($dynamic[images])) { foreach (json_decode($dynamic[images], true) as $fileUrl) { $this-deleteQiniuFile($fileUrl); } } // 删除动态主记录 Db::name(dynamic)-where(id, $dynamicId)-delete(); // 删除关联评论和点赞 Db::name(comment)-where(dynamic_id, $dynamicId)-delete(); Db::name(like_log)-where(dynamic_id, $dynamicId)-delete(); Db::commit(); return true; } catch (\Exception $e) { Db::rollback(); throw $e; } }注意事务只能保证数据库的一致性删除七牛文件失败不会回滚数据库因为外部存储不在事务边界内。常见的妥协策略是先删数据库再删 OSS 文件OSS 删除失败打日志由定时任务重试。如果使用模型关联定义comment和likeLog两个hasMany可以用with(comment,likeLog)-delete()自动执行关联删除但 TP 5.1 的关联删除每处理一个关联都会发一次 SQL数据量大时需要自己权衡。3.4 SQL 注入防护与 TP 5.1 的查询构造器边界TP 5.1 的查询构造器默认做了参数绑定where里的值传入不会直接拼接 SQL所以常规注入已经被框架拦截。但有一个例外order和field方法不支持参数绑定如果你二开时把前端传来的排序字段直接拼进去就会打开注入窗口。// 危险写法$sort 直接拼进 order Db::name(dynamic)-order($sort)-select(); // 安全写法白名单校验 $allowedSort [create_time desc, like_count desc]; $order $allowedSort[$sort] ?? [create_time desc]; Db::name(dynamic)-order($order)-select();如果要在 PHP 8 环境下运行 TP 5.1 老项目还需要注意一个兼容性细节PHP 8.0 移除了each()函数TP 5.1 的部分底层代码在特定场景会触发崩溃建议直接通过 Composer 升级topthink/framework到 5.1 的最新维护版本而不是停在最初发布的版本。另外 MySQL 5.7 对 group by 的 strict 模式前文提过二开聚合查询时尽量只查询分组字段和聚合函数字段。4. 七牛云存储接入与图片压缩链路4.1 为什么社区类项目把图片上云放在第一优先级朋友圈动态的核心内容是图片本地服务器存储图片最大的问题不是磁盘容量而是带宽。单张 2MB 的图片在一台 5Mbps 的服务器上要 3 秒才能加载完用户滑动时间线时并发拉取几十张图片直接打满出口带宽。源码选择七牛云存储解决了三个问题上行带宽释放、CDN 加速回源、图片实时压缩。本地只保留原始上传请求Uniapp 端把图片交给七牛后后端返回的只是七牛文件 key后续页面加载的 URL 全走七牛 CDN 域名。动态发布接口在写入数据库时图片字段存的就是https://img.yourdomain.com/xxxxx.jpg这种完整 URL这样查询端不用再做一次拼接。七牛在就近 CDN 节点缓存图片用户在多地域访问的延迟差异能控制在一倍以内。4.2 上传凭证签发与回调校验的完整流程七牛的上传凭证uploadToken不能由客户端直接生成因为签发凭证需要 SecretKey它只能放在 PHP 后端。源码在Upload控制器里实现了这个逻辑Uniapp 端先请求/index/upload/token拿到凭证再凭这个 token 直接上传文件到七牛。public function token() { $accessKey config(qiniu.access_key); $secretKey config(qiniu.secret_key); $bucket config(qiniu.bucket); // 自定义上传策略控制文件大小与 MIME 类型 $policy [ scope $bucket, deadline time() 3600, // token 有效期 1 小时 mimeLimit image/jpeg;image/png;image/webp;image/gif, fsizeLimit 5242880, // 单文件最大 5MB returnBody {key:$(key),hash:$(etag),w:$(imageInfo.width),h:$(imageInfo.height)}, callbackUrl https://yourdomain.com/index/upload/callback, callbackBody key$(key)hash$(etag)uid . $this-uid ]; // 签名算法Base64 URL 安全的编码 HMAC-SHA1 $encodedPolicy $this-base64UrlEncode(json_encode($policy)); $sign hash_hmac(sha1, $encodedPolicy, $secretKey, true); $encodedSign $this-base64UrlEncode($sign); $token $accessKey . : . $encodedSign . : . $encodedPolicy; return json([token $token, domain config(qiniu.domain), expires 3600]); }这里最关键的是mimeLimit与fsizeLimit如果漏掉这两个参数七牛会接受任意文件类型与任意大小攻击者可以把服务器存储当作免费网盘往 bucket 里拖几个 GB 的文件然后跑路。deadline控制 token 有效期前端在 onShow 时重新获取比保存一个 token 复用一天更安全因为过期后上传会直接 401。callbackUrl与callbackBody的作用是七牛收到文件后回调你的业务服务器把$(key)、$(etag)连同 URL 编码后的自定义字段传过来后端在这个接口里可以执行动态写入数据库的操作。注意如果你只配置returnBody而不配置callbackUrl用户上传成功后七牛直接把returnBody返回给客户端后端拿不到任何回调这适合客户端再单独请求一次后端接口完成业务入库的模式。4.3 图片压缩参数与样式分隔符的选型七牛云存储的图片压缩不用改原图它通过 URL 上的处理参数动态生成缩略图。这套源码用的处理参数基于 imageMogr2核心是 thumbnail 和 quality 两个参数参数示例效果适合场景thumbnail?imageMogr2/thumbnail/600x按宽度等比缩放高度自动朋友圈列表图quality?imageMogr2/quality/80压缩质量到 80%减少体积观感损失极小format?imageMogr2/format/webp转 webp 格式H5 端 Chrome 与微信内置浏览器auto-orient?imageMogr2/auto-orient修正 EXIF 旋转手机上传的图片必须加实际存储在数据库里的 URL 不要带这些参数查询时在 PHP 层统一拼接二开时如果要修改压缩策略只需要改一个公共函数不用去数据库里把每一条 URL 都刷一遍。H5 端使用image组件加载拼接后的 URL就能展示压缩后的图片。多尺寸需求列表小图、详情大图、头像裁切可以在公共函数里做一个尺寸映射表前端传场景名进来返回对应 URL比前端自行拼参数更可控。// 七牛图片样式拼接函数 function qiniu_thumb($url, $scene list) { $rules [ list ?imageMogr2/thumbnail/600x/quality/80, detail ?imageMogr2/thumbnail/1080x/quality/85, avatar ?imageMogr2/thumbnail/200x200/quality/80 ]; $rule $rules[$scene] ?? $rules[list]; return $url . $rule; }这种方式比七牛控制台里配的「样式分隔符」例如-list后缀更灵活因为分隔符方案需要提前在控制台定义样式二开时临时加一种尺寸得去控制台操作代码本身不可追溯。用 PHP 侧函数拼接的话所有规则沉淀在代码里Git 记录就是文档。4.4 H5 直传的 CORS 与 token 刷新策略Uniapp 编译成 H5 后uni.uploadFile请求发到七牛域名属于典型跨域请求七牛 bucket 需要开启 CORS 规则允许来源填写你的前端域名允许方法写GET, POST, PUT, OPTIONS允许请求头写Content-Type, Authorization, X-Requested-With。如果漏配 CORS浏览器会拦截 preflight 请求或最终响应表现为上传一直转圈然后 fail。还需要注意一个 token 过期边界七牛 token 有效期 60 分钟用户在文章编辑页停留超过这个时间再提交图片就会收到 401。常见做法是组件在每次上传前先校验本地 token 的过期时间戳小于 5 分钟就重新请求而不是依赖后端全局态。5. H5 兼容、小程序打包与二开扩展实践5.1 Uniapp 编译 H5 后嵌入公众号遇到的授权与定位问题这套源码主要兼容 H5实际跑起来最常见的场景是嵌入微信公众号菜单里。一旦作为网页授权应用微信内置浏览器的window.location.href跳转会丢失 session 状态如果源码中登录用 Cookie 做会话保持用户每次进入都可能被判定为未登录。所以二开的第一步是把 TP 侧登录态改成 token 机制Uniapp 端把 token 放入每次请求的Authorization头由TP的中间件统一校验。微信公众号里获取地理位置需要 JS-SDK 权限Uniapp 端通过script标签加载微信的jweixin-1.6.0.js然后调用wx.config注入签名信息。这里签名必须由后端用noncestr、timestamp、url三个参数计算url必须是当前页面完整路径含 hash小程序里由uni.getLocation直接调用即可H5 则只能走 JS-SDK。// uniapp H5 页面中初始化微信 JS-SDK // #ifdef H5 const wx require(/utils/jweixin.js) uni.request({ url: https://yourdomain.com/index/wechat/signature, data: { url: location.href.split(#)[0] }, success: (res) { wx.config({ debug: false, appId: res.data.appId, timestamp: res.data.timestamp, nonceStr: res.data.nonceStr, signature: res.data.signature, jsApiList: [getLocation] }) } }) // #endif注意location.href.split(#)[0]这个细节必须要 strip 掉 hash。微信签名校验的 URL 是不带 hash 的如果原样传入签名验证会失败并报 invalid signature这是社区里提问率很高的一个坑。另外签名接口不能放缓存后端每次都要用当前的timestamp重新计算计算完传给前端后签名只在 5 分钟内有效。5.2 Uniapp 打包微信小程序时的几个可复用经验Uniapp 打包成微信小程序时这套基于 H5 优先的源码有几个必须处理的点。首先是 uview-UI 1.x 在微信小程序端的 CSS 兼容问题——rpx单位在组件内部已经处理好但如果你二开新增的页面直接写了px在小程序端会偏小建议所有新页面统一使用 rpx。其次是小程序端的uni.uploadFile不能直接带Authorization头微信会在一些场景下过滤自定义 header需要改用uni.request携带临时文件路径或者在小程序的后台配置里为七牛域名开启「合法域名」白名单。小程序的图片上传前还有本地临时文件的问题chooseImage拿到的路径是wxfile://临时协议不能直接拿来展示在image中需要先用uni.saveFile转存或者接受临时路径的有效期限制。如果要实现 H5 与小程序双端同时支持朋友圈发布上传逻辑应该独立封装成一个混合上传方法根据// #ifdef H5与// #ifdef MP-WEIXIN分别执行不同分支。5.3 从朋友圈动态延伸出跑腿、支付模块的改造方向源码本身是社区朋友圈但作者在摘要里点明了它可以扩展成跑腿、支付这类带交易状态的程序。朋友圈的核心数据模型是一个无状态内容流跑腿订单则是典型的状态机流转待接单、已接单、配送中、已完成、已取消。改造的关键不是把dynamic表改造成订单表而是新建order表并建立与dynamic的弱关联订单备注可以发成一条动态。支付接入微信支付时后端notify回调接口必须放在独立控制器中且关闭鉴权因为微信服务器不会携带你的业务 token。在回调里验签成功后要更新订单状态与用户余额这两步操作必须放在事务里而且要保证幂等——微信会多次推送同一笔订单的回调不做幂等控制就会出现用户余额被加两次的事故。支付回调并不会阻塞核心业务建议用队列异步处理订单后续动作。5.4 ThinkPHP 5.1 老项目的安全加固清单TP 5.1 在互联网上已公开多种已知漏洞利用方式部署到外网前必须做三件事第一访问runtime目录禁止 Web 访问Nginx 配置里直接 location 拒掉runtime/与thinkphp/第二删除不必要的index.php入口文件暴露信息应用部署目录与 Web 根目录分离是更标准的做法第三升级到框架最新维护版因为旧版本对Request对象的filter[]参数处理存在 SQL 注入隐患。如果要从 PHP 7.1 迁移到 PHP 8.0TP 5.1 源码中可能触发弃用告警的写法集中在each()、create_function()与money_format()。建议在本地逐一跑php -l语法检测再对关键接口做一轮回归测试。若运维环境有更高的 PHP 要求优先考虑 TP 5.1 最后一个兼容版本或整体升级到 TP 6。6. 部署验证清单与 uniapp vue2 转 vue3 的注意点6.1 从 Vue 2 迁移到 Vue 3 时需要处理的兼容面这套源码是基于 uview 1.x 与 Vue 2 的 uniapp 项目如果哪天想把它迁到 Vue 3 版本先别急着改 UI 组件。uview 1.x 在 Vue 3 下直接不可用常规选择是替换为uview-plus它的 API 与 uview 1.x 大致对齐但少量属性名称有调整比如u-parse组件改成了内置组件rich-text按钮plain变更为variant。迁移前建议全局搜索u-开头的组件使用点逐一比对新老文档。Vue 3 里this的访问方式发生重大变化原本写在methods中的this.$u.http在组合式 API 中要从setup返回或者通过getCurrentInstance获取全局属性。操作这类老项目时的常用做法是在main.js里保留一个中间层兼容对象确保业务代码迁移时改动面可控。6.2 部署后应执行的四项验证拿到源码部署上线后我建议按如下顺序完成四步验证每一步都能定位一类问题验证项具体操作预期结果数据库初始化导入sql文件检查user、dynamic表是否有测试数据能正常查询表中无乱码七牛上传链路在 H5 端选择图片发布动态图片 URL 成功写入dynamic.images字段H5 定位在微信开发者工具中打开页面模拟授权能正常弹出授权并拿到经纬度小程序打包HBuilderX 云端打包上传到微信开发者工具预览页面正常加载图片走 CDN 域名可访问第二项最容易踩坑如果七牛的bucket没有设置returnBody而你在 PHP 侧等待$(key)上传回调会返回空字符串Uniapp 端表现为上传成功但列表页不显示。第三项适合用开发者工具的真机调试功能验证因为 PC 模拟器中的定位数据是由微信调试工具注入的和真机行为有差异。6.3 常见报错的快速定位步骤七牛上传报 401 时最优先排查的不是access_key而是服务器的系统时间是否偏移超过 5 分钟。七牛签名校验依赖 Unix 时间戳如果 NTP 没同步deadline已经是过去时间签名必然被拒。用date %s对比标准时间偏差过大即处理。动态列表接口返回 500 且页面无任何输出八成是application/database.php里debug关闭导致的错误信息被吞。临时打开show_error_msg true后重新请求把报错内容截图处理完再关掉。Uniapp 端出现「页面路径找不到」时检查pages.json的注册顺序与manifest.json中 H5 路由模式配置。history路由在 Nginx 直接访问二级页面会 404需要在 location 中配置try_files $uri $uri/ /index.html;或者把路由模式改回hash一劳永逸——这也是 H5 版朋友圈最常见的部署问题。本文还有配套的精品资源点击获取
返回列表