仿抖音用户作品数据解密:douyin 项目中 user_video_list/*.md 的数据结构、加载链路与生成脚本
【免费下载链接】douyinVue3 + Pinia 仿抖音,Vue 在移动端的最佳实践 . Imitate TikTok ,Vue Best practices on Mobile项目地址: https://gitcode.com/GitHub_Trending/do/douyin
导读
在 douyin(Vue3 + Pinia 仿抖音移动端项目)的public/data/user_video_list/目录下,存放着一批以.md结尾、实际却是7-Zip 压缩 JSON的"伪 Markdown"数据文件。它们承载着项目"我的页面 / 用户主页作品列表"的全部数据,从接口层被加载后直接以 JSON 解析并渲染。本文将以user-8357999.md为样本,完整拆解这份数据的内部字段结构、前端加载链路(src/mock/index.ts→src/api/user.ts→UserPanel.vue/Me.vue)、与 users.json 的关联方式,以及仓库中生成这些文件的 Node 脚本逻辑。读完后,你将能独立定位、解析并复现这套"压缩 JSON 伪装 Markdown"的数据管线。
一、文件本质:.md后缀下的 7z 压缩 JSON
1.1 为什么无法直接按文本读取
对public/data/user_video_list/user-8357999.md直接以 UTF-8 文本读取会立即失败。用十六进制查看文件头可看到特征签名:
7z bc af 27 1c 00 04 ...这正是7-Zip 归档格式的魔数(magic number)。也就是说,该文件并非 Markdown 文本,而是一个 7z 压缩包,包内只有一个同名的user-8357999.json文件。
说明:同一目录下的其他文件(如
user-04074747.md、user-LL991221.md、user-12345xiaolaohu.md等)以及 public/data/videos.md 都具有相同的 7z 文件头,说明这是项目统一的数据打包策略,而非个别文件损坏。
1.2 解压验证
使用 Python 的py7zr可以无损还原包内 JSON:
import py7zr with py7zr.SevenZipFile('public/data/user_video_list/user-8357999.md') as z: print(z.getnames()) # ['user-8357999.json'] z.extractall(path='/tmp/user8357999')解压出的user-8357999.json约 326 KB,是一个JSON 数组,包含 57 条抖音视频(aweme)记录。相比原始 JSON(约 326 KB),7z 压缩后的体积显著减小,这正是项目采用"压缩 + 伪装扩展名"来静态托管数据的原因——既能模拟接口返回,又能控制仓库体积。
二、数据结构全景:一条视频记录的 22 个字段
解压后的每一条记录都是一个平铺的抖音 aweme 对象,样本user-8357999.json首条记录包含以下顶层键:
| 字段 | 类型 | 含义 |
|---|---|---|
aweme_id | string | 视频唯一 ID |
desc | string | 视频文案/描述 |
create_time | number | 发布时间戳(秒) |
music | object | 背景音乐信息 |
video | object | 播放地址、封面、分辨率 |
share_url | string | 分享链接 |
statistics | object | 点赞/评论/收藏/分享等计数 |
status | object | 审核、删除、分享权限等状态 |
text_extra | array | 文案中的话题/@提及 |
is_top | number | 是否置顶(1 表示置顶) |
share_info | object | 分享文案与链接 |
duration | number | 视频时长(毫秒) |
image_infos | array | 图集信息 |
risk_infos | object | 风险/投票信息 |
position | object | 位置信息 |
author_user_id | number | 作者用户 ID |
prevent_download | boolean | 是否禁止下载 |
long_video | object | 长视频标识 |
aweme_control | object | 转发/分享/评论权限控制 |
images | array | 图片列表 |
suggest_words | object | 推荐词 |
2.1 视频播放信息video
video对象是前端播放与封面渲染的核心来源:
{ "play_addr": { "uri": "v0d00fg10000cj1lq4jc77u0ng6s1gt0", "url_list": [ "https://www.douyin.com/aweme/v1/play/?video_id=v0d00fg10000cj1lq4jc77u0ng6s1gt0&line=0&file_id=bed51c00899b458cbc5d8280147c22a1&sign=7749aec7bd62a3760065f60e40fc1867&is_play_url=1&source=PackSourceEnum_PUBLISH" ], "width": 1080, "height": 1920, "url_key": "v0d00fg10000cj1lq4jc77u0ng6s1gt0_h264_1080p_2053262", "data_size": 3480280, "file_hash": "7749aec7bd62a3760065f60e40fc1867" }, "cover": { "uri": "tos-cn-i-0813/oYVDeaFZyENAAAAKXCYfxD6hI4zADNAURgtySl", "url_list": ["jwWCPZVTIA4IKM-8WipLF.png"], "width": 720, "height": 720 }, "height": 3840, "width": 2160, "ratio": "1080p", "duration": 13560 }要点:
play_addr.url_list提供可直接播放的 URL;url_key记录了分辨率与码率(如h264_1080p);cover.url_list是封面图地址,注意这里被改写成了仓库内图片文件名(如jwWCPZVTIA4IKM-8WipLF.png),对应 public/images 下的静态资源;width/height/ratio用于前端做比例适配与清晰度选择。
2.2 互动数据statistics
{ "admire_count": 0, "comment_count": 21582, "digg_count": 1246636, "collect_count": 64460, "play_count": 0, "share_count": 172803 }digg_count:点赞数(该样本单条视频超过 124 万);comment_count/collect_count/share_count:评论、收藏、分享数;admire_count:赞赏数;play_count:播放数(部分来源为 0,前端会做兜底处理)。
这些字段直接驱动作品列表、视频卡片上的数字展示。
2.3 状态与权限status、aweme_control
{ "status": { "listen_video_status": 0, "is_delete": false, "allow_share": true, "is_prohibited": false, "in_reviewing": false, "part_see": 0, "private_status": 0, "review_result": { "review_status": 0 } }, "aweme_control": { "can_forward": true, "can_share": true, "can_comment": true, "can_show_comment": true } }前端通过status.is_delete、in_reviewing等决定是否渲染该条视频,通过aweme_control控制转发、分享、评论按钮的可用性。
2.4 背景音乐music
{ "id": 7123453673090321000, "title": "禁盗用", "author": "LoveW_", "cover_medium": { "uri": "720x720/aweme-avatar/...", "url_list": ["https://p3-pc.douyinpic.com/..."], "width": 720, "height": 720 }, "cover_thumb": { "uri": "100x100/aweme-avatar/...", "url_list": ["..."], "width": 100, "height": 100 }, "play_url": "...", "duration": 0, "user_count": 0, "owner_id": 0, "owner_nickname": "" }music提供 BGM 的标题、作者、封面缩略图与播放地址,用于视频播放器的音乐组件展示。
三、前端加载链路:从静态文件到组件渲染
3.1 接口定义 src/api/user.ts
项目在 API 层声明了用户作品列表接口:
export function userVideoList(params?: any, data?: any) { // 内部通过 axios/http 请求 '/user/video_list' }3.2 Mock 层拦截 src/mock/index.ts
由于项目没有真实后端,Mock 层用 axios-mock-adapter 拦截/user/video_list请求,并从静态数据文件读取:
mock.onGet(/user\/video_list/).reply(async (config) => { const id = config.params.id const r2 = await _fetch(`${FILE_URL}/user_video_list/user-${id}.md`) const v = await r2.json() if (v) { return [200, { data: v, code: 200 }] } return [200, { code: 500 }] })这段代码直接印证了文件名约定:请求user_video_list({ id })时,会加载user_video_list/user-${id}.md文件,并对 7z 内的 JSON 执行r2.json()解析。因此这里的.md扩展名对浏览器而言是"无妨的容器",真正起作用的是压缩包内的 JSON 内容。
另外,在"我的"页面接口video/my中,默认加载的是 user-12345xiaolaohu.md:
mock.onGet(/video\/my/).reply(async (config) => { const page = getPage2(config.params) if (!userVideos.length) { const r = await _fetch(BASE_URL + '/data/user_video_list/user-12345xiaolaohu.md') const list = await r.json() // ... 将 userList 中的作者信息合并进每条视频 userVideos = list.map((w) => { const item = userList.find((a) => String(a.uid) === String(w.author_user_id)) if (item) w.author = item return w }) } return [200, { data: { pageNo, total: userVideos.length, list: userVideos.slice(page.offset, page.limit) }, code: 200 }] })注意:
user-12345xiaolaohu.md与user-8357999.md是同构数据(均为 7z 压缩的 JSON 数组),且共用 public/data/users.json 中的用户信息做作者回填,说明这套数据格式在整个用户作品场景中是一致的。
3.3 组件消费 UserPanel.vue
用户主页面板通过userVideoList({ id })拉取该用户的全部作品:
import { userVideoList } from '@/api/user' // ... let r: any = await userVideoList({ id })随后把返回的list交给视频列表组件进行滑动播放;Me.vue 的"我的作品"Tab(name="videoList")同样消费这份数据。
四、数据如何生成:仓库中的 Node 处理脚本
public/data/user_video_list/*.md并非手写,而是由仓库node/目录下的脚本处理生成的。相关脚本包括:
- node/process-post-list.js:读取
node/post/data/下各用户的作品 JSON,按用户交叉排列后写出posts.json/posts6.json; - node/post/process-post.js 与 node/post/process-post-img.js:处理图文/视频帖子与图片;
- node/user/process-user.js、node/user/process-user-img.js:处理用户数据及头像图片;
- node/comment/process.js:处理评论数据;
- node/remove-dist-json.js:构建后清理 dist 中的 JSON;
- node/rename.js:批量重命名文件。
以 node/process-post-list.js 为例,其核心逻辑为:遍历post/data/目录下所有用户作品 JSON,记录每个文件的最大长度,然后按下标交叉取出各用户视频组成新列表,前 6 条写入posts6.json,其余写入posts.json,并通过users.find(a => a.uid == v.author_user_id)回填作者对象:
let data = newList.slice(0, 6) data.map((v) => { let u = users.find((a) => a.uid == v.author_user_id) if (u) v.author = u }) fs.writeFileSync('./posts6.json', JSON.stringify(data, null, 2)) fs.writeFileSync('./posts.json', JSON.stringify(newList.slice(6), null, 2))从脚本结构与输出文件命名可以推断:user_video_list/*.json为脚本的原始输入(抓取自抖音接口),随后被压缩为.md容器放入public/data/,供 Mock 层静态加载。
五、与公共数据文件的联动
user_video_list中的记录通过author_user_id与以下公共数据联动:
- public/data/users.json(及 users.md):用户基础信息,Mock 中通过
baseStore.users查找作者对象回填w.author; - public/data/posts.json:总帖子数据(当前为 62 条,字段如
id、model_type、note_card),是推荐流的数据源之一; - public/data/comments:评论数据,与视频
aweme_id关联; - public/data/videos.md:同样以 7z 容器存放的视频列表数据。
这种"数据集中托管在public/data/、Mock 层按接口模式读取"的做法,使项目在无后端的情况下完整复现了抖音 App 的信息流、用户主页、评论与搜索体验。
六、实操:如何在自己的分支中复用这套数据
- 解压任意用户数据:用
py7zr或 7-Zip 打开public/data/user_video_list/user-<id>.md,得到同名 JSON 数组; - 接入新用户:将新的 JSON 数组压缩为 7z 并命名为
user-<uid>.md放入public/data/user_video_list/,同时在 public/data/users.json 中补充对应用户信息(uid与author_user_id对应); - 前端联调:调用
userVideoList({ id }),Mock 层会自动匹配user-${id}.md并返回数据; - 重新生成聚合数据:修改
node/post/data/下的输入 JSON 后,运行 node/process-post-list.js 可重新生成posts.json系列文件(示例脚本使用 ES Module 与 fs 直接读写,运行环境为 Node 18+,详见 node/package.json)。
前提说明:以上数据管线描述基于当前仓库实际文件与脚本推断而来,脚本中
fs.writeFileSync的输出路径为脚本所在目录的相对路径,具体落地位置以仓库实际结构为准。
小结
user_video_list/user-8357999.md是 douyin 项目中一个颇具代表性的数据文件:以 7z 压缩 JSON 伪装 Markdown 扩展名,承载 57 条抖音作品的完整字段(播放、统计、状态、音乐、控制权限),经 src/mock/index.ts 的/user/video_list拦截加载,最终驱动用户主页作品列表渲染,并由node/下的脚本与users.json共同维护其生成与作者关联。理解这条管线,也就理解了整个仿抖音项目"静态数据模拟真实接口"的核心设计思路。
【免费下载链接】douyinVue3 + Pinia 仿抖音,Vue 在移动端的最佳实践 . Imitate TikTok ,Vue Best practices on Mobile项目地址: https://gitcode.com/GitHub_Trending/do/douyin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考