import{_ as s,o as n,c as a,a as l}from"./app.a81d7d4f.js";const u=JSON.parse('{"title":"从0实现一个年度报告","description":"","frontmatter":{},"headers":[{"level":2,"title":"实现难点","slug":"实现难点","link":"#实现难点","children":[{"level":3,"title":"1. 数据模拟","slug":"_1-数据模拟","link":"#_1-数据模拟","children":[]},{"level":3,"title":"2. 屏幕适配","slug":"_2-屏幕适配","link":"#_2-屏幕适配","children":[]},{"level":3,"title":"3. 动画效果","slug":"_3-动画效果","link":"#_3-动画效果","children":[]},{"level":3,"title":"4. 音乐播放","slug":"_4-音乐播放","link":"#_4-音乐播放","children":[]}]},{"level":2,"title":"用户数据","slug":"用户数据","link":"#用户数据","children":[{"level":3,"title":"用户数据内容","slug":"用户数据内容","link":"#用户数据内容","children":[]},{"level":3,"title":"数据模拟","slug":"数据模拟","link":"#数据模拟","children":[]}]},{"level":2,"title":"编码中遇到的问题","slug":"编码中遇到的问题","link":"#编码中遇到的问题","children":[{"level":3,"title":"音乐自动播放的问题","slug":"音乐自动播放的问题","link":"#音乐自动播放的问题","children":[]},{"level":3,"title":"VNode调整样式的问题","slug":"vnode调整样式的问题","link":"#vnode调整样式的问题","children":[]},{"level":3,"title":"监听Animation结束事件并更新响应式变量","slug":"监听animation结束事件并更新响应式变量","link":"#监听animation结束事件并更新响应式变量","children":[]},{"level":3,"title":"切换页面支持触控滑动滚轮键盘","slug":"切换页面支持触控滑动滚轮键盘","link":"#切换页面支持触控滑动滚轮键盘","children":[]},{"level":3,"title":"代码打包","slug":"代码打包","link":"#代码打包","children":[]}]},{"level":2,"title":"技术介绍","slug":"技术介绍","link":"#技术介绍","children":[]},{"level":2,"title":"功能介绍","slug":"功能介绍","link":"#功能介绍","children":[]},{"level":2,"title":"Demo展示","slug":"demo展示","link":"#demo展示","children":[]}],"relativePath":"article/从0实现一个年度报告.md","lastUpdated":1675779808000}'),p={name:"article/从0实现一个年度报告.md"},o=l(`

从0实现一个年度报告

每到年底各大应用都会推出自己的年终总结报告,统计出用户一年来在应用内的行为展示给用户,供用户记录、分享。

今年掘金社区推出了自己的2022掘友年度报告,这次我们仿照这个报告,从0开始自己实现一个年终总结报告页面

实现难点

1. 数据模拟

一般情况下是根据用户UID,到后端去请求相关接口获得统计数据。

例如掘金的接口为https://api.juejin.cn/event_api/v1/annual/annual_summary?aid=xxxxxx

本次后端使用NodeJS实现了一个爬虫,可以将用户数据统计完成后导出JSON格式的数据,将此数据粘贴到前端页面的输入框即可生成自己的报告

2. 屏幕适配

可以观察到,在PC端和在手机端访问年度报告展示的效果是不一样的。

本次考虑使用媒体查询来实现这个功能:

3. 动画效果

动画分为文本与背景元素的动画

背景动画容器的四个位置:左上角 右下角 中间部分 中间(悬浮气泡)。不同位置的动画容器都采用绝对定位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()更优雅的实现音频播放。实际情况是:浏览器禁用了无用户操作的音频自动播放,要想实现自动播放有两种解决方法:

为了保证功能实现的稳定性,我选择了前者。

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等全局状态放到其中管理非常方便,避免了provideinject的繁琐

功能介绍

Demo展示

Demo(Vercel)

jcode

`,59),e=[o];function t(c,r,F,D,y,i){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{u as __pageData,d as default};