mirror of
https://github.com/ZiuChen/ZiuChen.github.io.git
synced 2025-08-18 15:39:35 +08:00
227 lines
8.9 KiB
Markdown
227 lines
8.9 KiB
Markdown
# 从0实现一个年度报告
|
||
|
||
每到年底各大应用都会推出自己的年终总结报告,统计出用户一年来在应用内的行为展示给用户,供用户记录、分享。
|
||
|
||
今年掘金社区推出了自己的[2022掘友年度报告](https://zjsms.com/hbdA5jR),这次我们仿照这个报告,从0开始自己实现一个年终总结报告页面
|
||
|
||
## 实现难点
|
||
|
||
### 1. 数据模拟
|
||
|
||
一般情况下是根据用户UID,到后端去请求相关接口获得统计数据。
|
||
|
||
例如掘金的接口为`https://api.juejin.cn/event_api/v1/annual/annual_summary?aid=xxxxxx`
|
||
|
||
本次后端使用`NodeJS`实现了一个爬虫,可以将用户数据统计完成后导出JSON格式的数据,将此数据粘贴到前端页面的输入框即可生成自己的报告
|
||
|
||
### 2. 屏幕适配
|
||
|
||
可以观察到,在PC端和在手机端访问年度报告展示的效果是不一样的。
|
||
|
||
本次考虑使用媒体查询来实现这个功能:
|
||
|
||
- 宽屏则展示背景,页面切换也使用背景中的上下切换按钮
|
||
- 小屏则隐藏背景,让内容填满屏幕,页面切换通过滑动事件监听
|
||
|
||
### 3. 动画效果
|
||
|
||
动画分为文本与背景元素的动画
|
||
|
||
- 背景元素的动画使用了SVG动画
|
||
- 动画中不动的部分直接使用`.png`图片
|
||
- 运动的部分使用SVG动画绘制,如克里克的眼睛、尾巴
|
||
- 文本的动画使用了`CSS Animation`渐显的效果
|
||
- 不同段落之间通过`animation-delay`属性,彼此相差`1000ms`
|
||
|
||
背景动画容器的四个位置:`左上角` `右下角` `中间部分` `中间(悬浮气泡)`。不同位置的动画容器都采用绝对定位`position: absolute;`,辅以`z-index`实现层叠
|
||
|
||
囿于工期,本次的背景动画直接采用静态图片+`CSS Animation`实现上下浮动的效果
|
||
|
||
### 4. 音乐播放
|
||
|
||
通过`Audio`接口访问网络音乐链接,控制音乐相关功能
|
||
|
||
- 进入页面开始播放
|
||
- 离开页面暂停播放
|
||
- 支持点击按钮切换播放状态
|
||
|
||
## 用户数据
|
||
|
||
### 用户数据内容
|
||
|
||
```
|
||
- 用户名
|
||
- 注册时间 距今天数
|
||
- 创作相关
|
||
- 发布文章数
|
||
- 总阅读数 总赞数 总评论数
|
||
- 掘力值增长 超过人数百分比
|
||
- 最受欢迎的文章标题
|
||
- 社交相关
|
||
- 多少位掘友看过你的文章
|
||
- 点赞最多的掘友 评论最多的掘友
|
||
- 学习相关
|
||
- 阅读文章数 点赞数 评论数
|
||
- 总阅读字数
|
||
- 最关注的技术领域TOP3
|
||
- 沸点相关
|
||
- 发布沸点数 互动掘友数 点赞数 收赞数
|
||
- 互动最频繁的掘友
|
||
- 深夜阅读
|
||
- 最晚一次阅读时间
|
||
- 阅读的文章标题
|
||
- 早起阅读
|
||
- 最晚一次阅读时间
|
||
- 阅读的文章标题
|
||
- 最终总结
|
||
- 用户名
|
||
- 获得成就 饼图
|
||
```
|
||
|
||
### 数据模拟
|
||
|
||
根据不同页面分配不同的字段与数据,我根据自己的报告模拟了以下JSON数据
|
||
|
||
```json
|
||
{"pages":[{"id":0,"data":{"userName":"Ziu","enterDay":1647619200000,"tillNow":291}},{"id":1,"data":{"publishArticle":187,"getReader":78959,"getLike":393,"getComment":13,"increment":3693,"rate":0.9773,"mostPopularArticle":"深入理解浏览器缓存原理"}},{"id":2,"data":{"growFriends":2861,"mostLike":"老边","mostComment":"重载新生"}},{"id":3,"data":{"mostGoodAt":["JavaScript","Vue3","TypeScript"]}},{"id":4,"data":{"readed":1397,"like":1347,"comment":1345,"words":"244万","mostFocus":["前端","JavaScript","Vue.js"]}},{"id":5,"data":{"oftenRead":[{"name":"seloven","followed":0},{"name":"掘金酱","followed":1},{"name":"CLICK克里克","followed":1},{"name":"稀土君","followed":1},{"name":"cc123nice","followed":0}]}},{"id":6,"data":{"publishPin":395,"interact":1283,"like":705,"getLike":374,"mostInteractWith":"狗哥哥"}},{"id":7,"data":{"mostLateTime":1665768240000,"publishArticle":"深入理解浏览器缓存原理"}},{"id":8,"data":{"mostEarlyTime":1661208660000,"publishArticle":"深入理解Proxy与Reflect"}},{"id":9,"data":{"nickName":"Ziu","ability":"学习力","analysis":"滴水石穿,读百篇尽显敏而好学","medal":["与你同行","笔耕不辍","博览群文","高才掘学","前排围观"]}}]}
|
||
```
|
||
|
||
## 编码中遇到的问题
|
||
|
||
### 音乐自动播放的问题
|
||
|
||
之前我希望不操作DOM,而是通过`new Audio()`更优雅的实现音频播放。实际情况是:浏览器禁用了无用户操作的音频自动播放,要想实现自动播放有两种解决方法:
|
||
|
||
- HTML标签`<audio>`添加`autoplay`属性 后续播放/暂停都需要操作DOM
|
||
- 为界面添加监听器,监听到用户操作后手动触发`audio.play()`
|
||
|
||
为了保证功能实现的稳定性,我选择了前者。
|
||
|
||
```js
|
||
// useAudio.js
|
||
import { ref } from 'vue'
|
||
|
||
const defaultURL = 'https://xxxxxxxxxxxxxxxxxxxxxxxx.mp3'
|
||
|
||
export default function useAudio(url = defaultURL) {
|
||
const audio = new Audio(url)
|
||
audio.loop = true
|
||
|
||
const playStatus = ref(false)
|
||
|
||
const play = () => {
|
||
audio.play()
|
||
playStatus.value = true
|
||
}
|
||
|
||
const pause = () => {
|
||
audio.pause()
|
||
playStatus.value = false
|
||
}
|
||
|
||
return {
|
||
audio, play, pause, playStatus
|
||
}
|
||
}
|
||
```
|
||
|
||
### VNode调整样式的问题
|
||
|
||
项目使用的框架是Vue3,部分页面使用到了JSX语法,众所周知JSX语法定义的组件返回的是一个VNode树,要想统一为树上元素添加渐变的样式则需要遍历他们。
|
||
|
||
需要遍历树上所有元素,并分别为他们添加不同值的`animation-delay`属性:
|
||
|
||
```js
|
||
const children = DOM.children
|
||
for (let i = 1; i < children.length; i++) {
|
||
const child = children[i]
|
||
child.props
|
||
? (child.props.style = { animationDelay: `${i * 1000}ms` })
|
||
: Object.assign(child, {
|
||
props: { style: { animationDelay: `${i * 1000}ms` } }
|
||
})
|
||
}
|
||
```
|
||
|
||
`style`属性并不是VNode的直接属性,而放在`props`上的
|
||
|
||
### 监听`Animation`结束事件并更新响应式变量
|
||
|
||
如果当前页所有的动画都播放完毕,则需要将播放完的事件通知给JavaScript并将状态更新到响应式的变量中
|
||
|
||
在前文遍历DOM树时,为树中最后一个动画节点添加`animationend`监听回调,回调函数中执行`document.dispatchEvent(e)`,其中`e`是通过`const e = new CustomEvent('custom-animationend')`创建的自定义事件
|
||
|
||
回调触发时,在别处的`onMounted`回调内监听该自定义事件的触发,并更新响应式变量
|
||
|
||
### 切换页面支持触控滑动滚轮键盘
|
||
|
||
由于年终总结报告需要考虑到多种终端用户的体验,所以需要对翻页操作进行更多的优化
|
||
|
||
监听页面的触控滑动事件、滚轮滚动事件,并且匹配上翻页操作
|
||
|
||
```js
|
||
// 监听移动端滑动事件
|
||
let startY = 0
|
||
let endY = 0
|
||
document.addEventListener('touchstart', (e) => {
|
||
startY = e.touches[0].clientY
|
||
})
|
||
document.addEventListener('touchend', (e) => {
|
||
endY = e.changedTouches[0].clientY
|
||
if (endY - startY > 50) {
|
||
prevPage()
|
||
} else if (endY - startY < -50) {
|
||
nextPage()
|
||
}
|
||
})
|
||
|
||
// 监听鼠标滚轮事件
|
||
document.addEventListener('wheel', (e) => {
|
||
if (e.deltaY > 0) {
|
||
nextPage()
|
||
} else if (e.deltaY < 0) {
|
||
prevPage()
|
||
}
|
||
})
|
||
|
||
// 监听键盘事件
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'ArrowUp') {
|
||
prevPage()
|
||
} else if (e.key === 'ArrowDown') {
|
||
nextPage()
|
||
}
|
||
})
|
||
```
|
||
|
||
### 代码打包
|
||
|
||
由于最后需要将项目放到码上掘金平台运行,所以需要考虑静态资源的加载问题
|
||
|
||
这里我使用到了Vite提供的类似`file-loader`的功能,可以将大小在指定阈值下的图片资源直接转为行内的DataURL,配置选项是`config.build.assetsInlineLimit`,这样所有的图片资源都不必考虑外部引入的问题,直接内嵌进代码。
|
||
|
||
## 技术介绍
|
||
|
||
主界面使用的是Vue3的`SFC`,主要逻辑都在单文件组件中完成。通过`JSX`语法编写不同页面的内容,这样更方便我们为每个节点添加不同的动画。
|
||
|
||
`JSX`编写的组件通过全局注册后,在`SFC`中通过`<Component>`动态加载。
|
||
|
||
图片资源方面,使用到了雪碧图,部署后可以降低客户端发起HTTP请求频次,提高性能
|
||
|
||
代码复用方面,样式代码都抽离为单个的`xxxx.less`文件,哪里用到了直接导入即可
|
||
|
||
使用到了`Pinia`状态管理库,将`switching` `pageId` `audioStatus`等全局状态放到其中管理非常方便,避免了`provide`和`inject`的繁琐
|
||
|
||
## 功能介绍
|
||
|
||
- 支持PC端/移动端展示不同样式
|
||
- PC端左侧有控制栏 支持控制音乐播放 切换页面
|
||
- 移动端右上角控制音乐 滑动切换页面
|
||
- 页面之间切换有动态效果 文字逐行展示
|
||
- 左上角、右下角会出现矢量动图
|
||
|
||
## Demo展示
|
||
|
||
[Demo(Vercel)](https://juejin-annual-report.vercel.app/)
|
||
|
||
[jcode](https://code.juejin.cn/pen/7184691373911015459) |