
不瞒大家说我最近做了一件挺无聊但是又挺快乐的事让写代码的人工智能帮我写了个网页版音乐播放器然后把它部署到了莫一云上现在手机、电脑打开同一个网址就能听歌歌词还能跟着歌曲逐行高亮。整个项目做完我最大的感受不是“AI真厉害”而是另一个更实际的结论——AI编码工具可以随便选真正折腾人的是部署平台。1. 这个播放器项目是怎么长出来的很多人好奇现在手机上什么听歌软件都有为什么还要自己折腾一个播放器。说实话不是闲得慌是被现成播放器给烦到了。我平时听歌习惯很固定就是听本地下载好的音频文件不听推荐流也不看评论。最近几年各大播放器首页越来越热闹推荐、榜单、直播、短视频全往首页堆我只想“听完一首下一首”结果得在一堆内容里找播放列表。时间久了就动了念头干脆自己做一个反正现在AI写代码这么方便。1.1 让我无法忍受的需求痛点先列三个我实际遇到的痛点这个项目就是围绕它们来的。第一我正在听的歌不能被后台切掉。用手机浏览器打开网页听歌只要切到别的App或者网络稍微抖一下网页里的音频经常就停了。这个问题到后面做部署时尤其明显音频资源的加载路径、跨域策略都会影响播放的稳定性。第二歌词对我来说是刚需。我听歌有个习惯一定要跟着歌词走不然很多歌听完也不知道唱的什么。可是主流播放器的歌词要么藏在会员功能里要么和评论区混在一起看着累。我就想要一个纯歌词页面字应该在哪一行那一行就高亮。第三播放进度要能记住。有一阵子我迷上了听长音频一期内容四五十分钟今天听了十分钟明天打开居然从开头重新放真的很打击人。所以我这个播放器一定得有进度记忆刷新页面也好换设备也好都得记得我听到哪了。1.2 功能清单最终长这样经过几轮调整最终功能清单定成了这些播放/暂停、上一首/下一首支持列表循环和随机播放进度条可拖拽拖到哪播到哪歌词逐行高亮支持LRC格式的歌词文件记住每个文件的最后播放进度刷新后自动续播支持PC和手机的浏览器不用装App不用登录界面干净没有推荐没有广告不采集任何行为数据这些功能听起来不多但真要让AI写出来并且写稳还是有几个关键点需要把握的。1.3 为什么选了H5而不是桌面或者App在设计形态时我有过三个选择桌面软件、安卓App、网页版H5。桌面软件我用Electron套壳写过最大的问题是更新麻烦、跨平台要分别打包折腾一通后发现需求其实没那么重。安卓App就更不用说了光签名、渠道、权限适配就够喝一壶。最后选了H5网页播放器理由很朴素浏览器是每个设备都自带的东西只要部署在公网上手机和电脑打开同一个地址就能用不用装任何东西。这个选择看起来简单却直接影响后面的技术方案。网页播放器意味着所有交互都在前端完成后端只需要提供静态文件和音频资源部署架构就变得很轻。也正因为轻才有后面折腾部署平台的各种空间。2. AI编码工具那段为什么可以“随便选”我核心想说的一句话是AI编码工具可以随便选但这个“随便”是有前提的前提是你把问题拆得足够清楚。这次用的AI编码工具是Codex。说实话选它没什么特别高深的理由就是因为听说它终端交互做得不错能直接在项目目录里读懂代码、按任务改代码改完还能自动跑测试和lint。至于模型我把它接到了DeepSeek的接口上用起来也很顺手。2.1 Codex接入DeepSeek省去来回换工具的折腾Codex默认连的是OpenAI自己的模型但它是支持自定义模型提供方的。我的诉求很简单——不想因为API访问问题耽误干活所以我把它切成了DeepSeek。安装Codex本身就是个命令行的事npm install -g openai/codex # 或者用Homebrew # brew install codex codex --version装完之后配置在用户目录下的配置文件中。我直接新建了一个config.toml把默认模型指向DeepSeekmodel deepseek-chat model_provider deepseek [model_providers.deepseek] name DeepSeek base_url https://api.deepseek.com/v1 env_key DEEPSEEK_API_KEY wire_api chat然后在系统环境变量里加入DEEPSEEK_API_KEY。这里有个小细节要注意不同版本的Codex对配置字段要求可能有细微差异如果填错它启动时会提示按提示改就行。我当时第一次就把base_url写成了不带/v1的地址结果请求一直报错后来看了文档才发现DeepSeek的OpenAI兼容接口带路径。这么配置之后Codex在写代码时就是通过DeepSeek的模型来理解和生成代码了。日常写前端页面、调逻辑、写Dockerfile完全够用。2.2 让Codex听话的三个提示小技巧很多人用AI写代码觉得不听话我观察下来大部分是因为提示语写得太笼统。你让它“写一个播放器”它只能想到播个MP3不会想到你要歌词联动、进度记忆这些刁钻需求。我这次总结出三个比较有用的提示方法。一是把需求拆成小任务一次只让AI改一个点。比如“先写一个能播放第一首歌的页面”“现在加上播放列表的数据结构”“给进度条加上点击跳转功能”。每次只改一个点AI出错的概率会大大降低代码也更容易审查。二是要主动告诉它“不许做什么”。比如我的播放器是纯前端项目我就明说“不要引入后端数据库”“不要用任何需要登录鉴权的方案”“不要改我已经写好的样式文件”。圈定边界比提一堆功能更管用AI很少会自作主张超出去。三是让它把改动说明写清楚。我会在Codex里要求它把本次改动点列出来这样我在看diff时心里有数哪些是它自己脑补的哪些是真正需要的。2.3 写代码过程中真正浪费时间的点这里必须说句公道话AI写代码虽然快但也让我踩过不少隐形的坑。最典型的场景是AI会生成看起来非常完整的代码但一运行就报错而且报错信息看着和问题完全对不上。拿播放器项目举例Codex第一次生成的列表切换逻辑在浏览器里点击“下一首”时页面会整页刷新我排查了半天最后发现它用了一个form包着按钮按钮默认触发了表单提交。这种问题AI自己很难发现因为它不在浏览器里看行为只看代码逻辑。还有一个浪费时间的点是它在把歌词解析和音频播放联动时会把时间线写得过于复杂搞出一个“看起来严谨”的状态机来。后来我简化了方案用时间戳排序加指针推进代码量少了三分之一运行还更稳。这个经验后面会细说。所以我的结论是AI编码工具可以随便选选哪个都能写出个能跑的东西但你要做好自己还得理解核心代码的准备。不要当甩手掌柜AI是副驾方向盘得在自己手里。3. 播放器最关键的两段代码音频进度和歌词同步播放器的代码形态不复杂就是一个纯前端的Vite项目页面由HTML、CSS和JavaScript组成。但里面有两个地方最考验人一个是对音频播放进度的掌控一个是歌词逐行同步。3.1 页面结构和播放基础页面结构其实很简单左侧是播放列表右侧是封面和歌词区底部是播放控制条。核心播放能力用的是浏览器自带的Audio对象这玩意儿兼容性好也够稳。播放状态、时间更新、播放结束都通过事件监听来处理const audio new Audio(); audio.src currentTrack.url; audio.play(); audio.addEventListener(timeupdate, () { // 更新进度条和歌词 updateProgress(); syncLrc(); }); audio.addEventListener(ended, () { playNext(); });这里一定要用timeupdate事件因为它是浏览器在播放位置变化时触发的适合用来驱动进度条和歌词同步。我见过有人用setInterval每秒去读一次currentTime最后歌词也好、进度条也好都有不同程度的卡顿和漂移体验很糟。3.2 歌词同步不漂移的关键歌词同步这块我愿称之为本项目最大的坑。LRC歌词文件长这样[00:00.00]歌名 [00:06.50]第一句歌词 [00:12.30]第二句歌词 [00:18.42]第三句歌词解析起来不难关键是同步策略。第一次让Codex写的时候它用的是“每次timeupdate触发就遍历所有歌词行找时间最接近的”。这个方案在小歌词文件里勉强能用但遇到歌曲时间长了歌词多了遍历次数越来越多会出现高亮滞后也就是屏幕上的歌词和声音对不上的情况。后来我换成了指针推进法核心逻辑是把歌词按时间从小到大排好记录当前下标每次播放时间前进只和下一句歌词的时间比较到了就切换否则维持不变const lines parseLrc(lrcText); let currentLineIndex 0; function syncLrc() { const current audio.currentTime; while (currentLineIndex lines.length - 1 lines[currentLineIndex 1].time current) { currentLineIndex; } // 高亮 lines[currentLineIndex].text renderLrc(lines[currentLineIndex].text, currentLineIndex); }这个方案的时间复杂度基本是O(1)无论歌词多长计算量都不变而且因为用的是同一个audio.currentTime时间源不会出现状态错乱。3.3 Codex在这里犯过的初版错误Codex初版写歌词解析时用了一个很“聪明”的正则去匹配时间戳// 初版代码问题很多 function parseLrc(text) { return text.split(\n) .map(line line.match(/\[(\d{2}):(\d{2})\.(\d{2})\](.*)/)) .filter(Boolean) .map(m ({ time: Number(m[1]) * 60 Number(m[2]) Number(m[3]) / 100, text: m[4].trim() })); }逻辑本身没太大问题但它在遇到多时间戳的行时会出错比如[00:12.30][00:45.20]副歌这种重复歌词LRC格式里很常见代表同一句在多个时间点出现。初版正则只能匹配第一个时间戳第二段时间就听不到歌词高亮了。改法是先用全局匹配把行内所有时间戳抽出来每个时间戳单独对应一句。这类细节不实际跑一遍歌真的很难发现。我的体会是AI能给你一个“看起来能跑”的版本但真正的坑大多藏在你不常遇到的边角数据里。写播放器这种依赖浏览器API和音频格式的项目一定要用真实音频文件、真实歌词文件去测不要只用造出来的假数据。4. 部署平台才需要认真挑莫一云是我对比下来的选择如果说“AI编码工具随便选”是我这次的一个结论那另一个更加刻骨铭心的结论就是部署平台不能随便选。代码写错了我随时能改平台选错了后续的迁移成本、域名重新绑定、数据搬运一桩桩都是实打实的时间黑洞。4.1 为什么部署和写代码是两码事AI编码工具解决的问题是怎么把需求变成代码。但代码不等于服务中间隔着一整条部署链路——静态资源怎么托管、域名怎么解析、HTTPS证书怎么配、对象存储怎么挂载、容器重启后数据还在不在、日志去哪看、更新后怎么回滚。这一连串问题AI编码工具它们不管它们只能在代码层面帮你改。所以我宁可在最开始花点时间把部署平台选明白也不希望后面三天两头搬家。我甚至觉得对个人项目来说部署平台选得好不好直接决定了这个项目最后是长期运行还是一个一次性demo。4.2 三类部署方案的取舍我实际把方案过了一遍最后浓缩成三类可选方向。方案优点缺点适合场景自购云服务器手动装环境自由度最高想装什么装什么运维成本高安全组、Nginx、进程守护全得自己搞有经验且需要长期折腾的人容器托管/PaaS平台部署快日志、监控、扩缩容都现成平台有厂商绑定换个平台要重新梳理个人项目、中小型应用纯静态托管最简单上传文件就行不支持服务端跨域策略难调没法跑容器纯展示页面这个播放器项目虽然以静态文件为主但我希望后续能通过容器的方式统一管理并且可能需要挂载存储卷来存放音频。纯静态托管满足不了这个需求自购服务器又太重了所以我把重点放在容器托管平台方向。4.3 莫一云胜出在哪选莫一云不是因为它名气响是因为它几个点刚好踩中我的需求。第一点是部署链路够简单。我把Docker镜像推上去平台就能直接跑起来端口、日志、监控面板都有现成的不需要我手动去配置一堆系统服务。第二点是国内节点访问速度快。我人在国内播放器是要在真实网络环境里用的如果服务部署在海外节点延迟和稳定性都是问号。莫一云的国内节点保证了打开页面和加载音频的体验在可接受范围内。第三点是对对象存储和持久卷的支持。我这种应用音频文件不能跟容器一起跑必须单独存放容器重启不能丢数据。莫一云对接对象存储比较方便这正好满足我的核心诉求。第四点是成本低个人项目不需要上大规格一个小容器就够跑了。当然我也没有把所有资源都堆在上面毕竟只是自己用没必要过度配置。我实际也踩过一味贪平台功能多的坑云厂商那套Kubernetes全家桶对于一个小播放器来说就是杀鸡用牛刀配置起来人先疯掉。个人项目选平台核心就在“简单、稳定、数据安全”这几个词上。5. 莫一云部署全程Docker加Nginx把播放器推上线部署过程我没有选择从零开始手动搭服务器环境因为播放器是一个典型的静态前端项目最省心的打包方式就是用Docker把Nginx和构建产物包在一起之后推到莫一云上作为一个容器应用运行。5.1 本地构建镜像Dockerfile的写法前端项目用Vite构建最终的产物是一堆静态文件。我写了一个多阶段构建的Dockerfile第一阶段装依赖、跑构建第二阶段把构建好的dist目录复制进Nginx镜像FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:1.27-alpine COPY --frombuild /app/dist /usr/share/nginx/html COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]这段Dockerfile尽量分开了“构建”和“运行”两层之间互不干扰。最终运行时镜像里没有源码也没有node_modules只有Nginx和静态文件镜像体积极小推送和启动都很快。5.2 资源和端口配置在莫一云上创建应用时我选了容器部署方式资源选了个小规格够播放器用就行。端口配置这里要特别留意——容器里Nginx监听的是80端口平台规则里也需要把对外端口指向容器的80否则页面会一直打不开而且这种问题不看部署日志很难发现。我当时的操作链路是本地把镜像构建好推送到镜像仓库然后在莫一云后台创建应用选择该镜像指定端口80点击部署。整个过程大概几分钟日志从拉取镜像、启动容器、Nginx ready一步步刷出来非常直观。5.3 绑域名、上HTTPS以及音频文件到底放哪播放器做完肯定不能用一个IP去访问那样难看也不稳定所以我绑定了一个自己的域名。在莫一云后台把我唱片机的域名解析到平台给的访问地址然后配置HTTPS证书。这个不细说了各家平台大同小异只要域名解析正确、证书生效HTTPS顺手就开了。更关键的是音频文件的存放位置。我有两个选择一个是直接把音频文件打进Docker镜像这个最懒但最不推荐因为音频动辄几十MB镜像会越来越大每次更新都要全量推一遍而且容器重建时音频会被清掉。另一个是把音频放对象存储播放器通过HTTPS直接访问对象存储上的文件。我最后选了对象存储方案。前端镜像里只放页面和歌词文件音频文件全部单独传到对象存储。相应地播放器里的音频地址改成对象存储的完整URL。这样做的好处是清晰更新前端版本时音频不受影响也不用打包下载大文件。需要提醒的是对象存储默认不一定允许跨域访问如果页面域名和文件域名不一致浏览器会拦截。我在对象存储的跨域设置里显式允许了页面域名的访问[ { AllowedOrigin: [https://music.example.com], AllowedMethod: [GET, HEAD], AllowedHeader: [*] } ]配置完后用浏览器开发者工具能直接看到音频请求正常返回不会再报CORS错误。6. 上线后踩到过的坑以及我是怎么定位的这部分我本不想写因为每个坑都让我多花了一两个小时的调试时间但这些坑也确实最能体现“部署才需要挑平台、但挑完还是要学会和平台相处”这件事。我按排查链路一条一条写希望能帮后来人省点时间。6.1 音频文件不发声先查CORS第一次在线上打开页面界面正常、列表正常进度条也动了但就是没有声音。浏览器控制台报了一串类似“Access to audio at ... from origin ... has been blocked by CORS policy”的错。我当时的排查链路是这样的先在本地开发环境试同样的代码在本地是好的音频能放说明问题不是代码逻辑而是线上环境和本地环境有差异。再看线上页面域名和音频域名果然是两个不同的域名浏览器跨域限制拦住了音频请求。最后去对象存储的跨域配置里加上了页面域名的允许访问规则问题解决。这个过程让我想起做前端工作时的老经验本地能跑的不代表线上能跑而线上和本地最大的区别往往不是代码而是网络策略、域名、跨域这类环境因素。容器平台决定运行环境但跨域策略是你自己得负责的配置。6.2 中文文件名的歌词和音频404我的音频文件名是中文的比如《夜曲.mp3》《晴天.mp3》在本地一切正常但到线上的Nginx环境里有部分中文资源加载不了日志里看到404。一开始我以为是路径写错后来发现是Nginx在处理中文文件名时如果前端生成的资源路径没有做URL编码就会出现编码不一致导致找不到文件。前端代码里需要给文件路径做编码处理把中文转成%E5%A4%9C%E6%9B%B2.mp3这种形式或者统一在服务端配置URL解码规则。这个坑在本地开发时很难触发因为本地开发服务器往往做了更多的兼容处理但到了独立的Nginx容器里很多“宽容”就不存在了。说到底还是那句老话——真实环境才是唯一标准。6.3 容器重建后音频文件消失这个坑其实前面已经预告过了但我还是完整写一下。最初图省事我把一部分测试音频直接放进镜像的/usr/share/nginx/html/audio目录里跟前端一起打包部署。当时觉得小无所谓。后来容器因为更新镜像被重建了一次上去一看那些测试音频全没了页面里图片和页面还在但所有音频请求全部404。当时我心里一瞬间是懵的随后就明白了容器就是一堆临时进程镜像的层才有持久性业务数据放容器里本来就是不合理的。解决办法就是前面说的把音频挪到对象存储让容器保持“无状态”。这个坑如果没踩过真的很难意识到但踩过一次之后就再也忘不掉了。还有一次遇到过更阴间的现象就是旧版本镜像被回收后播放记录还在但音频对象存储里的文件也没了一检查发现是自己手动清理过桶没注意前缀过滤。所以后来我形成了习惯对象存储上新建一个单独的目录专门存放这个项目的音频其他项目绝不混用清理时也不会误伤。最后再分享一个我自己惯用的维护习惯项目上线不算结束反而算开始。我养成了两个小习惯现在觉得特别值。一个是每次部署前先在本地完整跑一遍不只跑页面还要真实播放两三首歌切几次歌、调整几次进度条确认核心链路没问题才推线上。另一个是上线后定期看一眼平台的日志和监控面板不需要多频繁隔三差五看一次确认CPU、内存和访问日志都在正常范围心里有底。这次做播放器最值得分享的其实不是代码而是我对“AI编码工具和部署平台选型”这件事的新认识。工具的差别远没有想象中那么大选哪个都行真正决定一个项目能不能长期稳定运行的是部署平台的可靠性、持久化方案和跨域策略这些底层能力。AI写代码给了我们把想法快速变成项目的能力但想把项目真正变成一个每天都能打开的服务还是得靠自己在部署这条路上多上点心。