前期感受
说起来,好久没做c端项目了,上一次应该还是在5年前,在公众号组做影视红包的时候,这次被指派负责官网,虽然网站一期只为推出品牌,所以页面很简单,甚至只有一个接口用来切换背景视频,但还是有点诚惶诚恐,主要是toC的项目,问题容易被放大,比如下面:
- 怕业务突然挂了,自己也没发现,缺少前、后端告警;
- 怕兼容性问题,毕竟c端的设备千奇百怪,每次调试的都很头疼,造成项目延期;
- 后端接口虽然只有一个,但整个后端devops部署流程一样都少不了。
项目调研阶段
前端选型
因为是一个前端较重的项目所以先对前端进行了框架选型,本着每一次新项目都是一次学习的原则,再加上因为是c端项目,所以理论上ssr方案应该是最优解。于是从next和nuxt中间进行选择,因为不喜欢react给开发者增加的一些心智负担,再加上nuxt3.0也开始支持了vue3,最终选择了nuxt。
后端选型
后端需要提供一个接口,逻辑是从b站四名主播中随机抽取一人,返回他当前的直播状态,这种简单的业务场景,不需要创建一个完整的后端项目,云函数的非常符合这类场景,但是腾讯云的云函数,实际体验下来有种被放弃的感觉,表现在于:
- 云函数的vscode插件已经不维护了,尝试用vscode push函数失败。
- 文档提供的Serverless Cloud Framework方案只支持到node12。
- 最重要的是不支持cors配置,需要再接入云网关才能有这个能力,而一个网关一年要900。
云开发≠云函数
柳暗花明又一村,发现云开发中也有云函数,且云开发支持cors功能。(感觉csig降本增效,切到大动脉了),所以最终采用云开发功能
项目Demo
后端
为了跑通最小闭环,当时还未和b站人员对接,先用爬虫的方式实现接口,也可作为项目的Plan B,后端需要一个定时器,一个redis,一个返回给前端的服务,这些通过云函数都可以做得到,下面是定时器的核心代码:
通过获取直播页面的html,然后正则匹配live_status后面的状态码。
// 核心函数,通过 const INPUT = [{ name: '星瞳_Official', room: 'https://live.bilibili.com/22886883' },{ name: '学英语', room: 'https://live.bilibili.com/24167384', }, { name: '听wo姐说', room: 'https://live.bilibili.com/27311555' }]; async function getYoutuber(item) { const yt = { ...item } const body = await new Promise(async (resolve, reject) => { try { const res = await fetch(item.room); const body = await res.text() resolve(body) } catch (error) { reject(error) } }) const reg = RegExp('\"live\_status\"\:(\\d*)'); const liveStatus = body.match(reg)[1]; console.log(liveStatus); yt.liveStatus = liveStatus youtubers.push(yt); return yt }
前端
基本就是用nuxt3的cli工具生成一个,但遇到了一些问题
- generate的项目和公司的eslint规范无法同时配置
- 项目有一个pc端页面,一个m/的移动端页面,需要在浏览器请求的时候服务端根据设备http_user_agent进行分别重定向,但是由于前端是准备放在腾讯云的cos中,没有nginx这一层,所以无法做到分别重定向。只能将nuxt的改为前端渲染应用csr,但未来考虑扩展。
- nuxt的NuxtImg指令,大部分优化能力都需要项目开启ssr,但因为上面的问题,导致这个指令的优化大部分都用不了
组件库的选择
起初希望用上shadcn-vue,但是遇到了如下报错无法解决:
Importing directly from module entry-points is not allowed.
应该是shadcn生成的组件导入方式写法有问题。最终选择了nuxt ui。
但用了nuxt ui后发现它依然不够好,主要是功能不够完善。就比如UModal组件,我自定义一个关闭按钮,希望手动触发click时触发close,在文档上找不到,只能通过获取model的ref关闭。
新一代组件库
不管是shadcn还是nuxt ui都是基于
Headless UI
的设计思路,将样式与组件交互分离开,甚至都没有样式。不像以前的组件库采用bem的方式写样式,当需要修改样式时只能通过覆盖原有样式。其中shadcn是基于最早的headless,Radix UI上做了进一步封装,也是现在比较火的项目,但可能最早做的是react的,所以vue的兼容性还没做好。动画库的选择
我尝试了animate.css,@vite/auto-animate,vue默认的animate,最终还是用了vue自带的transition,auto-animate的自动交互感觉场景比较垂直,比如列表的增删,或者一些它示例里的那些场景,如果自定义,会发现dom的位置变动不合理。
设备识别
这里使用了
bowser
库,可以根据UA返回client的设备,可以识别微信,但是会对ipad的mini和pro返回不同的platform。如何定义素材尺寸
这里背景视频和图片我的解决方案是:只区分两种,手机和pc,ipad作为pc端的一个变种,对背景进行位移处理。
移动端的ui素材:9 :16比例,750:1334分辨率
pc端的素材:考虑到浏览器有导航+标签栏,采用21:9或者2:1的比例,可以考虑用:3440×1440(4k)或者2560×1080(2k)的。具体根据素材大小进行取舍。
全屏背景(视频/图片)策略
跟设计同学沟通,我们没有过场动画,直接加载背景,这里横向比较了几个主流游戏官网,普遍视频小于10mb,哪怕是有过长动画,要考虑有时候网络环境并不好的情况。另外就是对于素材占满全屏的肯定是需要一个保留原素材宽高比的方案,即不能简单的占满全屏。这里采用一套策略:
const VIDEO_SIZE = [3840, 2160]; // 这里用视频的宽高比 const materialRatio = videoOriginSize[0] / videoOriginSize[1]; const decideWidthOrHeight = () => { // 找出原图的最小边 const screenRatio = window.innerWidth / window.innerHeight; if (screenRatio > materialRatio) { return 'w-full'; } return 'h-full'; }; <video v-if="videoUrl" id="background-video" ref="videoRef" loop muted autoplay playsinline webkit-playsinline x5-video-player-type="h5" x5-video-orientation="landscape" x5-playsinline="x5-playsinline" :poster="posterUrl" :src="videoUrl" class="select-none object-cover object-top" :class="[decideWidthOrHeight()]" > <source :src="videoUrl" type="video/webm" /> </video>
彩蛋设计
希望在视频上出现可以交互的元素,这里也借鉴了一下其他的网站(https://ispy.heihei.resn.co),主要难点就是相对定位,因为背景图片/视频会进行缩放,那么彩蛋的位置也需要跟着变动,效果如下:
// 根据原始彩蛋的左上角0点位置以及大小,去计算最终的位置 const EASTER_EGG = [ { x: 310, y: 237, size: [227, 468], bgImage: '/_nuxt/assets/images/egg-1.png', }, { x: 500, y: 860, size: [325, 410], bgImage: '/_nuxt/assets/images/egg-2.png', }, ]; const calcCurrentScale = () => { if (!eggs) { return []; } if (videoOrientation.value === 'landscape') { scale.value = window.innerWidth / videoOriginSize[0]; return eggs?.map((egg) => { return { x: egg.x * scale.value, y: egg.y * scale.value, size: egg.size.map((len) => len * scale.value), bgImage: egg.bgImage, }; }); } else { scale.value = window.innerHeight / videoOriginSize[1]; return eggs?.map((egg) => { return { x: egg.x * scale.value, y: egg.y * scale.value, size: egg.size.map((len) => len * scale.value), bgImage: egg.bgImage, }; }); } }; // 通过hover时实现放大图片覆盖原来位置的原图,并增加动画,产生彩蛋的错觉 <template> <img v-for="(egg, index) in eggsScaled" :key="index" :style="`width:${egg.size[0]}px; height: ${egg.size[1]}px; top: ${egg.x}px ; left: ${egg.y}px;`" :src="egg.bgImage" class="when-hover absolute bg-cover bg-center bg-no-repeat" /> </template> <style scoped lang="scss"> .when-hover { transition: all 0.3s; opacity: 0; &:hover { transform: scale(1.2); filter: drop-shadow(0px 0px 20px rgb(9, 187, 189)); opacity: 1; } } </style>
微信下的视频自动播放
这里依赖js-bridge的能力,进行了配置
// 微信兼容自动播放 if (window.WeixinJSBridge) { WeixinJSBridge.invoke('getNetworkType', {}, function (e) { const videoDom = document.getElementById('background-video'); videoDom?.play(); // audioRef.value?.play(); }); } else { document.addEventListener('WeixinJSBridgeReady', function () { WeixinJSBridge.invoke('getNetworkType', {}, function (e) { const videoDom = document.getElementById('background-video'); videoDom?.play(); // audioRef.value?.play(); }); }); }
通过这种hack的方式可以自动播放视频+音频,但是在chrome这些浏览器下是无法使用的。并且video的autoplay也是会被浏览器拦截。所以要想自动播放在所有场景下,最好是增加一个点击交互。让用户去手动触发play()事件。
正式开发
还好我在demo阶段做了大量工作,将一些核心的功能已经提前都做完。因为距离上线期前一个星期设计稿才全部给到,等我开发完pc和移动端的页面开发后,只剩2~3天的时间了,还要进行测试和部署,真是有够无语……
b站接口
起初和b站的对接人沟通一直是已读不回,指导上升问题后才得到反馈,最终通过沟通,还是通过司内的兄弟部门,跟他们进行对接。因为他们已经跑通了一整套直播平台的直播流对接
app、微信朋友圈、朋友分享
这里我们配置了og协议(https://www.yuanx.me/blog/posts/open-graph-protocol/)
meta: [ { property: 'og:title', content: '《虚环》官方网站- 谁是最强的虚拟偶像?' }, { property: 'og:description', content: '虚环首曝主题站,谁是最强的虚拟偶像?虚环是一款以虚拟偶像、模拟经营和都市幻想为主题的动作策略演出游戏,游戏中的角色原型均为现实中存在的虚拟偶像。玩家需要在游戏中扮演一位神秘的侦探,收集、培养并组建自己的偶像团体,通过参加VFC最强偶像争霸战,揭开关于比赛和世界的黑暗真相', }, { property: 'og:image', content: 'https://vo.qq.com/img/logo.png' }, { property: 'og:url', content: 'https://vo.qq.com' }, { property: 'wechat:title', content: '《虚环》官方网站- 谁是最强的虚拟偶像?' }, { property: 'wechat:description', content: '虚环首曝主题站,谁是最强的虚拟偶像?虚环是一款以虚拟偶像、模拟经营和都市幻想为主题的动作策略演出游戏,游戏中的角色原型均为现实中存在的虚拟偶像。玩家需要在游戏中扮演一位神秘的侦探,收集、培养并组建自己的偶像团体,通过参加VFC最强偶像争霸战,揭开关于比赛和世界的黑暗真相', }, { property: 'wechat:image', content: 'https://vo.qq.com/img/logo.png' }, { property: 'wechat:url', content: 'https://vo.qq.com' }, ]
但这里微信比较特殊,它会管控分享的图文标签,一般的h5分享,不会形成图卡。要想支持,必须接入公众号鉴权,接入js-sdk,也就是需要一整套公众号授权服务。这是我刚开始给忽视的,导致最后要上线时才去匆忙跑流程,最后也无法支持此功能。
对象存储作为页面访问
之前并没有配置过对象存储作为静态网站,还是踩了一些坑的,比如桶要配置成私有读写,通过cdn去访问、要开启静态网站功能,并在cdn加速那里配置静态网站源站。
整个项目的数据情况
总结
上面就是我尽量总结这个项目从从零到一的开发过程,涵盖了前后端技术选型、项目调研、开发挑战及解决方案,以及需要关注的方方面面。