ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

无后端短视频小程序实战:Mock数据与广告变现完整解析

无后端短视频小程序实战:Mock数据与广告变现完整解析

简介:这是一份抖音测试小程序完整源码包,主要面向想快速上线抖音小程序、尝试流量主变现的个人开发者或小团队。源码无需自建后台,配置好抖音开放平台参数即可部署,适合已掌握基础前端知识、希望缩短从开发到上线周期的学习者。压缩包共236个文件,约1.31MB,以gif动图、png图片、js逻辑、json配置、ttss/ttml抖音样式与模板文件为主,并附带搭建教程docx,文件类型覆盖界面、交互、数据配置等环节,目录结构较完整。目前已有1920人学习下载。价值方面,包内含可直接运行的测试小程序、流量主接入相关配置、常用素材与说明文档,可帮助理解抖音小程序轻量实现思路,减少从零搭建的时间成本,也适合用于学习小程序开发流程、研究流量主收益逻辑,以及作为个人项目的起步模板。

1. 一个不要后台的短视频小程序:导入即跑,广告位就是现金流水

短视频信息流小程序最麻烦的通常不是 UI,而是数据从哪来。某开发者要交付一个短视频小程序 demo,后端排期两周,UI 评审和广告位验收却迫在眉睫,于是有了这套无需后台的测试小程序。它把短视频业务前端闭环做完了:上下滑视频流、点赞、关注、评论、个人中心,再集成平台流量主广告组件。导入开发者工具,填上 AppID 就能跑,真机预览可看到完整交互;亲测可用还意味着改一行配置,就能把 mock 数据换成真实接口。它适合小程序入门者学结构,适合产品经理验证交互,也适合研究广告变现逻辑的开发者。整份源码零服务端代码,跑通的是整套本地测试链路,上线前的商业化验证都在这条链路里完成。

2. 架构与目录:没有服务端,数据到底存在哪

2.1 无后端架构的取舍:本地 Mock 与平台能力

这套项目的核心设计思路是“前端自洽”。所有需要网络的地方,都用本地 mock 数据或平台能力替代。视频文件放在 static 目录里用绝对路径引用,用户信息、视频列表写在一个 JS 模块中,页面直接 import。这样带来的直接好处是:不需要配 HTTPS 域名、不需要租服务器、不需要处理接口鉴权,clone 下来就能跑,零成本进入调试状态。

但它不是空壳。看数据流能发现它模拟了真实业务的完整链路:视频列表来自 mock 文件,播放状态由页面栈管理,点赞和关注写入本地 Storage,广告位调用的是平台原生广告组件。这套模式最接近真实生产环境的地方是“配置分离”——所有可替换的数据源都收敛在 services 和 mock 两个目录中。我评审这类项目时会先看一点:数据请求层和页面渲染层是否解耦。这个项目在这点上做得干净,VideoService、UserService 都是统一入口,页面不关心数据来自文件还是来自网络。后续做二次开发时,只需要替换 Service 内部的实现为 request 请求,页面几乎不用动。

另外,测试小程序最怕的是“偏科”——把精力全放在视觉上,却没考虑状态怎么复位、数据怎么重置。这个项目在 storage 层做了统一封装,所有写操作都走 services/storage.js,清理测试数据只需要调用一个 clear() 方法。对于要反复验收 UI 和广告位的场景,这个设计才是真正能提高效率的部分。

2.2 目录结构:从启动页到视频流的代码组织

将项目解压后,源码目录结构如下:

src/ ├─ app.js # 全局入口,初始化缓存与全局数据 ├─ app.json # 页面注册、tabBar、组件注册 ├─ app.wxss # 全局样式变量 ├─ pages/ │ ├─ index/ # 视频信息流主页(启动页) │ ├─ detail/ # 视频详情与评论区 │ ├─ profile/ # 个人中心页 │ └─ web/ # 内嵌 web 页(外链备用) ├─ components/ │ ├─ video-item/ # 单个视频卡片(标题、作者、点赞条) │ ├─ ad-banner/ # banner 广告位封装 │ └─ ad-reward/ # 激励视频广告封装 ├─ services/ │ ├─ video.js # 视频列表/详情接口 │ ├─ user.js # 用户信息与关注状态 │ └─ storage.js # Storage 统一封装 ├─ mock/ │ ├─ videos.js # 视频列表数据 │ └─ users.js # 创作者数据 └─ utils/ └─ config.js # 全局配置(AppID、开关、域名)

每个页面是标准的 wxml / wxss / js / json 四件套。pages/index 是启动页,也就是 home feed。app.json 里注册了所有页面、组件和 tabBar,pages/detail 通过页面参数接收视频 ID,再从 services 重新拉详情。这个设计让分享成为可能——参数带在路径里,其他人打开 detail 页就能定位到同一条视频。

模块文件职责后续替换时的关注点
services/video.js暴露 getVideoList / getVideoDetail换后端时把内部改成 wx.request
mock/videos.js返回约定好的视频数组字段名与页面绑定,替换时保持约定
services/storage.js封装 Storage 读写与清理接后端时把缓存迁移为服务端状态
components/ad-bannerbanner 广告位unitId 需要替换为自己的流量主广告位
utils/config.js全局开关、预留域名、广告位 ID上架前重点核对这一份文件

2.3 启动流程与数据流:页面加载时做了什么

从 app.js 的 onLaunch 开始,整个链路是这样的。onLaunch 只做两件事:初始化全局数据,从 storage 读历史用户信息;调用一次 services 的初始化方法,为信息流页面预热数据。

pages/index 页的 onLoad 用异步方式拉取视频列表和用户信息,拿到后渲染第一帧:

// pages/index/index.js — 信息流首页初始化 const { getVideoList } = require('../../services/video') Page({ data: { videoList: [], currentIndex: 0, loading: true }, async onLoad() { // getVideoList 当前返回 Promise,内部用 setTimeout 模拟 300ms 网络延迟 const videoList = await getVideoList() this.setData({ videoList, loading: false }) } })

getVideoList 在 services 层把 mock 数据包装成异步返回,页面代码完全按照真实接口的写法来。这里有几个关键点:页面不直接 require mock 文件,而是经过 services 层,保证后续替换接口时页面零改动;setData 只更新视频列表和 loading 状态,currentIndex 由滑动事件驱动。300ms 延迟对应弱网 loading 态的测试需要,换真实接口后删除模拟延迟即可。

视频流容器用 swiper 的竖直模式实现,每个 swiper-item 放一个 video 组件,autoplay 由 currentIndex 控制:

<!-- pages/index/index.wxml — 竖直滑动信息流 --> <swiper vertical="true" bindchange="onSwiperChange"> <swiper-item wx:for="{{videoList}}" wx:key="id"> <video id="video_{{item.id}}" src="{{item.videoUrl}}" autoplay="{{index === currentIndex}}" controls="{{false}}" object-fit="cover" show-center-play-btn="{{false}}" /> </swiper-item> </swiper>

bindchange 回调里做两件事:更新 currentIndex,并暂停上一个视频。关键参数逐个说:vertical="true" 把横向轮播变成竖直滑动,这是短视频交互的核心;autoplay 绑定 index === currentIndex,保证只有当前可见的 swiper-item 在播放,其他视频处于暂停状态,避免多视频同时解码导致的卡顿;object-fit="cover" 让视频铺满容器,防止内容区出现黑边;show-center-play-btn 置 false,去掉点击时默认弹出的播放按钮,保留页面自定义的交互元素。

这套播放状态管理的成本很低,但已经把“自动播放、手动暂停、滑动切换”三件事都覆盖到了。真机调试时如果发现滑动卡顿,优先检查是不是有多个 video 在同时解码——比如 autoplay 没有绑死、离开页面时没有调用 stop。

3. 核心模块拆解:从滑动视频到广告曝光的完整链路

3.1 信息流交互:滑动切换、自动播放与节流

index 页的滑动切换,除了 swiper 自带的滚动行为,还需要处理业务逻辑。onSwiperChange 拿到当前索引后,先取出旧视频的实例并 stop,再让新视频进入 autoplay 状态:

// pages/index/index.js — 滑动切换时的播放控制 onSwiperChange(e) { const { current } = e.detail const oldIndex = this.data.currentIndex // 先暂停上一个视频,避免多个 video 同时解码造成卡顿 if (oldIndex !== current) { const oldVideo = wx.createVideoContext(`video_${this.data.videoList[oldIndex].id}`, this) oldVideo && oldVideo.stop() } this.setData({ currentIndex: current }) }

这里有一个细节值得留意:createVideoContext 的第二个参数要传 this,不然在自定义组件场景下会找不到对应实例。旧视频 stop 之后再更新 currentIndex,新视频的 autoplay 才会在数据变化后生效。整个切换流程是“旧停、新播、更新索引”,顺序不能反,反了容易出现两个视频同时出声的翻车现场。

点赞、关注这类高频交互,我一般会做一层节流。短视频场景下用户手指很快,如果每个点击都直接 setData 到视图层,低端机上的响应会明显滞后。这里的做法是点击后立即改内存状态,setData 丢给视图层,同时异步写入 storage:

// services/storage.js — 点赞状态写入与读取 const LIKE_KEY = 'like_records' function getLikeMap() { return wx.getStorageSync(LIKE_KEY) || {} } function setLike(id, liked) { const map = getLikeMap() if (liked) { map[id] = true } else { delete map[id] } wx.setStorageSync(LIKE_KEY, map) }

storage 封装暴露了三个方法:getLikeMap、setLike、clear。页面层不直接操作 Storage,所有读写都收敛到这里,方便统一做数据迁移。参数上注意两个约定:键名用 like_records,值是对象映射而不是数组,这样查询单条视频是否点赞是 O(1) 操作;删除用 delete 而不是置 false,避免无意义的脏数据膨胀。

3.2 数据驱动与状态管理:页面怎么感知点赞变化

短视频信息流里列表页和详情页可能同时存在,点赞状态要保持一致。这个项目用了一个简单的事件广播来做跨页同步:

// utils/event-bus.js — 极简事件总线 const listeners = {} function on(event, cb) { if (!listeners[event]) listeners[event] = [] listeners[event].push(cb) } function emit(event, payload) { listeners[event].forEach(cb => cb(payload)) } module.exports = { on, emit }

信息流页在 onLoad 时注册 on('like-change'),详情页在点赞成功后 emit('like-change', { id, liked: true }),列表页收到事件后更新对应视频的状态。这个事件总线没有做 remove 逻辑,但页面 onUnload 会自动清空回调数组。跨页状态同步的另一种做法是全局 getApp().globalData 存一份状态,页面 onShow 时重新读取,但那样每次切页都要全量刷新,成本反而高。

事件总线的数据流链路是“详情页修改 → storage 落盘 → emit 广播 → 列表页刷新子组件状态”。这套链路的好处在于,所有状态变更都有唯一入口,测试时出问题能沿着事件名快速定位。比如点赞数对不上,先查是不是直接操作了 data 而没有走 setLike。

3.3 广告位接入:Banner 与激励视频的实现细节

广告位接入是这个项目商业化部分的核心。banner 广告封装在 components/ad-banner 里,使用平台原生广告组件:

<!-- components/ad-banner/index.wxml — banner 广告位 --> <view class="ad-container" wx:if="{{showAd}}"> <ad unit-id="{{adUnitId}}" ad-type="banner" binderror="onAdError"></ad> </view>

组件对应的 js 里,adUnitId 来自 utils/config.js 的配置:

// components/ad-banner/index.js const config = require('../../utils/config') Component({ data: { adUnitId: config.bannerAdUnitId, showAd: true }, methods: { onAdError(err) { // 广告拉取失败时静默隐藏,不影响视频播放 this.setData({ showAd: false }) } } })

这里的关键参数是 adUnitId。开发阶段用的是平台提供的测试广告位,提审前必须换成自己流量主后台创建的正式广告位。换成正式广告位后,如果广告组件报错,基本上可以判断是资质没开通或者广告位类型填错。另外 banner 的加载次数不要做太多逻辑,平台侧已经做了频控,前端只需要控制位置和显隐。激励视频的做法类似,但触发场景通常绑定在用户主动点击“解锁 XX”的按钮上,不能放在启动页或自动弹出。

激励视频的接入方式补全如下:

// components/ad-reward/index.js — 激励视频广告 showRewardAd() { if (wx.createRewardedVideoAd) { const ad = wx.createRewardedVideoAd({ adUnitId: config.rewardAdUnitId }) ad.show().catch(() => { ad.load().then(() => ad.show()) }) } }

激励视频的注意点是“失败重试”:show() 失败后需要主动调用 load() 再 show(),否则用户授权拒绝或网络波动时广告直接消失。完整的激励视频回调还应该监听 onClose,根据 isEnded 参数决定是否发放奖励,这个参数决定了用户有没有看完完整视频,是防刷的核心依据。

4. 本地跑通:导入、配置、真机预览全流程

4.1 导入开发者工具与 AppID 配置

把项目 zip 解压,用开发者工具“导入项目”选择 src 目录。AppID 这一步可以填自己的正式 AppID,也可以填测试号。用测试号的好处是不用管 appid 的权限问题,但广告组件会走测试广告位逻辑。工具导入成功后,先看编译是否通过,控制台有没有报错。常见的导入失败原因是目录选择错误——把整个 zip 解压后的文件夹选进去了,而不是 src 目录,路径里没找到 app.json 就会直接报错。

这个项目的音视频资源路径都是相对路径,导入后不用额外做资源拷贝。如果你发现视频无法播放,先检查 static 目录是否完整,再检查编译日志里有没有文件缺失警告。开发者工具左侧的“代码质量”面板会列出所有未使用的文件和异常引用,建议在跑通前先扫一遍。

4.2 关闭后端依赖:保持 Mock 数据源

默认情况下项目完全跑在 mock 数据上。需要确认几个位置:utils/config.js 中 useMock 保持 true;services 层没有额外的 wx.request 调用;视频路径指向 static 下的本地文件。这三个条件满足时,工具编译后看到的首页视频应该就是 mock 里的第一条。

把 useMock 开关周围的 config 展示一下:

// utils/config.js — 全局配置 module.exports = { useMock: true, // 置 false 后走真实接口 apiBase: 'https://api.example.com', // 预留真实域名 bannerAdUnitId: 'adunit-test-banner', // 测试 banner 广告位 rewardAdUnitId: 'adunit-test-reward', // 测试激励视频广告位 videoDomain: 'https://cdn.example.com' // 预留视频 CDN 域名 }

useMock 这个开关贯穿所有 services 方法。改接口时先不要把 useMock 改掉,而是先让 services 层同时保留两套实现,联调完成后再整体切换。测试阶段保留这个开关的另一个好处:UI 回归、广告位检查、评审演示都能在一个稳定数据源上重复进行,不会被接口字段变动搞挂。

4.3 真机预览与提审检查

工具编译通过后,点预览会生成真机预览二维码。首次真机预览建议开启调试模式,方便在 vConsole 里看 log。真机上确认几件事:视频能正常播放;滑动切换时不会有黑屏;点击点赞关注后状态能保持;广告位在测试模式下能渲染。真机预览和工具模拟器的差异往往集中在视频解码和广告加载上,模拟器能跑不代表真机没问题。

提审前再过一遍合规清单:隐私弹窗是否展示;用户授权是否在触发时申请;信息流页是否有分享按钮;广告组件是否有遮蔽内容的情况。这些不是后端能帮你兜底的事情,纯前端检查项。实际操作时我会把清单直接写进项目的 README 里,每次提审照着过一遍,比临时翻代码靠谱得多。

5. 常见问题与避坑:广告不显示、域名拦截与白屏

5.1 广告组件渲染不出来

现象:真机上广告位区域一片空白,开发工具里偶尔能显示,上线包完全不出现。原因分三种:流量主资质未开通;用的是正式广告位 ID 但账号没权限;基础库版本太低不支持广告组件。解决:先在 config 里换成测试广告位 ID,确认组件能渲染;再看工具控制台的 ad 错误码;最后检查账号后台的流量主状态,未开通的账号即使真机预览也不会返回广告。

5.2 视频文件加载失败被拦截

现象:开发工具正常,真机上视频一直转圈,控制台报域名校验失败。原因是平台的安全策略要求所有请求域名配置在合法域名列表,本地绝对路径在开发工具被跳过,真机校验则严格生效。解决:开发阶段可以在工具详情面板勾选“不校验合法域名”;正式上架必须把视频文件放到 HTTPS 域名下并在后台配置 downloadFile 合法域名。这里容易踩的坑是“开发工具能跑就不管了”,结果审核人员一打开就是白屏,本质上还是域名问题。

5.3 真机预览白屏或卡启动页

现象:工具编译正常,真机扫码后白屏几秒甚至一直白屏。原因通常是基础库版本太低,或者视频组件在初始化阶段就尝试解码一个较大的 mp4。解决:工具右上角切换调试基础库到较新版本;把 mock 里的视频文件压缩到几 MB 以内;启动页加封面图占位,视频加载完成后再替换。如果是老旧机型一直白屏,可以考虑用 cover-view 做兜底文案。

5.4 修改 Mock 数据后不生效

现象:改了 mock/videos.js,重新编译,首页还是旧数据。原因大概率是 storage 缓存,点赞状态和用户信息被存到了本地缓存里,页面启动时优先读了缓存。解决:开发阶段调用一次 storage.clear(),或者工具面板选择“清缓存并重新编译”。这背后的教训是:mock 数据和缓存数据要分开看,改数据前先搞清楚读的是哪一份,否则容易白费时间。

6. 二次开发:把测试壳换成真实业务

6.1 一条配置开关拆掉 Mock 与线上

二次开发最值得投资的是“可切换数据源”。先看 services/video.js 怎么改造成双数据源:

const config = require('../utils/config') const mockVideos = require('../mock/videos') async function getVideoList() { if (config.useMock) { // 保留模拟延迟,保证 loading 状态可测 await new Promise(resolve => setTimeout(resolve, 300)) return mockVideos.videos } const res = await new Promise((resolve, reject) => { wx.request({ url: `${config.apiBase}/video/list`, success: resolve, fail: reject }) }) // 接口字段映射成页面约定的字段名 return (res.data.list || []).map(item => ({ id: item.vid, coverUrl: item.cover, videoUrl: item.play_url, title: item.title, likeCount: item.like_count, commentCount: item.comment_count })) }

替换真实接口后,重点验证三件事:字段映射是否正确;分页参数是否传了;异常时是否给用户兜底。我一般习惯在 config 里再加一个 forceError 开关,联调时一键让接口返回失败,验证页面的错误态是否正常展示。功能上线后把 useMock 置 false 切线上,再把 mock 目录作为回归基线保留。

从那以后我每次接新项目,都强制要求先搭好这个 mock 开关,再开始写业务代码。联调不顺时切 mock 先看是页面问题还是接口问题,省掉大量互相甩锅的时间。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进