夜听城嚣 夜听城嚣
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏

dwfrost

前端界的小学生
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏
  • 读书笔记

  • 专题分享

  • 项目实践

    • 圣诞节活动总结
      • vite踩坑之旅
      • vue3踩坑之旅
      • typescript实践
      • 迁移支付宝小程序踩坑记录
      • electron制作小程序上传工具总结
    • 框架应用

    • 前端一览
    • 项目实践
    frost
    2021-12-24

    圣诞节活动总结

    # 项目复盘

    这次圣诞节活动需求有如下特点:

    • 花样多,有许多非常规功能,比如播放音乐,摇一摇,震动,动画等等

    • 时间紧,时间线如下

      节点 时间
      被通知参与开发 12.13
      需求评审 12.13
      (设计稿未出)技术调研+搭建工程 12.13-12.15
      联调(当天给出接口文档) 12.16
      提测 12.16
      上线 12.23

    此次开发暴露前端一些问题:

    • 不能快速选型技术方案,搭建工程
      • 工程模板在vue2+ts和uniapp摇摆选择。
      • 前者是之前搭建过的项目模板,代码规范和全家桶配置比较成熟,不过 h5 涉及的原生功能需要自己开发 。
      • 后者是 uniapp 的 h5 模板,项目组之前有较多的 uniapp 开发经验,大部分可以复用,uni 上 API 丰富。不过这个模板的项目依赖比较老,个人内心对于 uniapp 的工程配置 和目录结构不是很认可。
    • 开发原生功能经验不足,技术调研花费较多时间,技术细节见下文。
    • 没有提前找后端对接口文档。不过本次业务需求较简单,略过。

    # 雪花效果

    先上最终效果。

    snow

    代码很简单,是纯 css 实现,缺点是雪花不能自定义修改,只能整个图片替换。

    .snow-wrap {
      // 雪花
      position: fixed;
      inset: 0;
      z-index: 1;
      background: url('../assets/snow1.png'), url('../assets/snow2.png');
      -webkit-animation: snow 10s linear infinite;
      animation: snow 10s linear infinite;
      pointer-events: none;
      @keyframes snow {
        0% {
          background-position: 0 0, 0 0;
        }
        100% {
          background-position: 500px 1000px, 500px 500px;
        }
      }
      @-webkit-keyframes snow {
        0% {
          background-position: 0 0, 0 0;
        }
        100% {
          background-position: 500px 1000px, 500px 500px;
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    图片如下

    snow2

    snow1

    整个方案是最简单的,拿来即用。之前在 github 找到一些雪花效果的代码,如下:

    let it snow (opens new window)

    canvas-confetti (opens new window)

    snowflake (opens new window) (github (opens new window))

    基于 snowflake,自己做了些优化,可以自定义雪花图片,大小,飘落速度等,同时提供开始,暂停、继续和停止等动作,见demo 地址 (opens new window)

    # 播放音乐

    注意:h5 由于浏览器兼容性,不能自动播放,需要用户手动点击交互后,才可以调起 API 播放。因此,我们做了一个透明蒙层,覆盖首页,用户第一次点击之后,就可以播放音乐了 。与此同时,可以监听 onShow 和 onHide 来控制用户是否在当前页时播放,否则即使不在当前页,音乐会一直播放(打扰到用户)。

    直接使用 uni.createInnerAudioContext 的 API 即可。完整代码如下

    <template>
      <div class="AudioBtn-wrap" :class="{ on: isPlaying }" @click="switchAudio"></div>
    </template>
    <script>
    export default {
      name: 'AudioBtn',
      components: {},
      data() {
        return {
          audio: null,
          isPlaying: false,
        }
      },
      created() {
        this.initAudio()
      },
      methods: {
        initAudio() {
          const innerAudioContext = uni.createInnerAudioContext()
          this.audio = innerAudioContext
          innerAudioContext.loop = true
          innerAudioContext.src = require('@/assets/mp3/bg.mp3')
          // innerAudioContext.autoplay = true // h5部分浏览器不支持自动播放
          // this.switchAudio()
          innerAudioContext.onPlay(() => {
            console.log('开始播放')
          })
          innerAudioContext.onPause(() => {
            console.log('暂停播放')
          })
          innerAudioContext.onError((res) => {
            console.log('onError', res)
            console.log(res.errMsg)
            console.log(res.errCode)
          })
        },
        switchAudio() {
          if (this.isPlaying) {
            this.audio.pause()
          } else {
            this.audio.play()
          }
          this.isPlaying = !this.isPlaying
        },
        play() {
          if (this.isPlaying) return
          this.audio.play()
          this.isPlaying = true
        },
        pausePlay() {
          if (!this.isPlaying) return
          this.isPlaying = false
          this.audio.pause()
        },
      },
    }
    </script>
    <style lang="scss" scoped>
    .AudioBtn-wrap {
      width: 50rpx;
      height: 50rpx;
      background-size: contain;
      background-image: url(../assets/images/sound-silent.png);
      &.on {
        background-image: url(../assets/images/sound.png);
      }
    }
    </style>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68

    由于产品要求各个地方有俏皮音效,比如点击按钮音效等,抽取出来一个 audio 类。

    const map = {
      button: require('@/assets/mp3/button.mp3'),
      wish: require('@/assets/mp3/wish.mp3'),
      gift: require('@/assets/mp3/gift-fall.wav'),
      shake: require('@/assets/mp3/shake.mp3'),
    }
    class Audio {
      constructor() {
        this.audio = null
        this.isPlaying = false
        this.initAudio()
      }
      initAudio() {
        const innerAudioContext = uni.createInnerAudioContext()
        this.audio = innerAudioContext
    
        // innerAudioContext.loop = true
        // innerAudioContext.src = require('@/assets/mp3/bg.mp3')
    
        innerAudioContext.onPlay(() => {
          console.log('开始播放')
        })
        innerAudioContext.onPause(() => {
          console.log('暂停播放')
        })
        innerAudioContext.onError((res) => {
          console.log('onError', res)
          console.log(res.errMsg)
          console.log(res.errCode)
        })
      }
      startAudio(src) {
        this.audio.src = src
        if (this.isPlaying) {
          this.audio.pause()
        } else {
          this.audio.play()
        }
        this.isPlaying = !this.isPlaying
      }
    
      playButton(type) {
        if (this.audio.src !== map[type]) {
          this.audio.src = map[type]
        }
        this.audio.play()
      }
      stopButton(type) {
        this.audio.src = map[type]
        this.audio.pause()
      }
    }
    
    export default new Audio()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54

    # 摇一摇

    参考蒋宇捷大佬的文章 (opens new window),封装了 shake.js。基本原理是:监听 x,y,z3 个方向的加速度,当其达到一定阈值时,认为是 摇一摇的动作。由于 iOS 有兼容性问题 (opens new window),需要加个按钮进行降级处理。

    此外,iOS 还有个神奇的体验 bug,如果用户在之前有过输入框输入,在摇晃手机时会弹出“撤销键入”弹窗提示,这简直要了亲命。经测试,发现网上的所谓切换页面和监听 blur() 不管用,摸索出如下 2 个兜底方案:

    • 在旁边提示用户,去设置关掉该弹窗,设置路径:设置-辅助功能-触控-摇动以撤销
    • 监听路由或者直接在 created 中刷新页面:window.location.reload()。当然,体验就不那么友好了。
    const SHAKE_THRESHOLD = 3000
    let lastUpdate = 0
    let x
    let y
    let z
    let lastx
    let lasty
    let lastz
    
    /**
     * 收集加速度数据,判断是否达到摇一摇的状态
     * 核心:针对三个方向的加速度进行计算,间隔测量它们,考察它们在固定时间段里的变化率,而且需要为它确定一个阈值来触发动作。
     * @param {Object} acceleration
     * @returns
     */
    export function listenShake(acceleration) {
      const curTime = new Date().getTime()
    
      if (curTime - lastUpdate > 100) {
        const diffTime = curTime - lastUpdate
        lastUpdate = curTime
    
        x = acceleration.x
        y = acceleration.y
        z = acceleration.z
    
        const speed = (Math.abs(x + y + z - lastx - lasty - lastz) / diffTime) * 10000
    
        if (speed > SHAKE_THRESHOLD) {
          return true
        }
        lastx = x
        lasty = y
        lastz = z
      }
      return false
    }
    
    export function checkPermissionForShake() {
      // navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate
      // showToast(navigator.vibrate ? '支持设备震动!' : '不支持震动', { duration: 10000 })
    
      if (typeof DeviceMotionEvent.requestPermission === 'function') {
        DeviceMotionEvent.requestPermission()
          .then((permissionState) => {
            // console.log('permissionState', permissionState)
            if (permissionState === 'granted') {
              // 授权允许
              uni.startAccelerometer()
            }
          })
          .catch((err) => {
            console.error(err)
            // iOS需要用户进行交互后弹出授权框
            uni.showModal({
              content: '请授权使用摇一摇功能',
              confirmColor: '#b3272c',
              success: function(res) {
                if (res.confirm) {
                  // console.log('用户点击确定')
                  checkPermissionForShake()
                } else if (res.cancel) {
                  // console.log('用户点击取消')
                }
              },
            })
          })
      } else {
        // ios其他系统可以不通过请求直接摇一摇
        uni.startAccelerometer()
        // console.log('other iOS')
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73

    使用如下

    import { listenShake, checkPermissionForShake } from '@/utils/shake'
    
    created() {
    	this.shake()
    },
    methods: {
        shake() {
          checkPermissionForShake()
          uni.onAccelerometerChange(res => {
            if (listenShake(res)) { // 摇一摇
              // iOS兼容性很差 iphone11,12,13不支持震动,android支持
              uni.vibrateLong()
              audio.playButton('shake')
            }
          })
        },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    # 海报生成

    直接在插件市场找到一个人气较高的海报组件 (opens new window),发现作者还整了一个组件集合,也贴出来, 见Lime Ui (opens new window)。此外,如果要寻找最新示例,可以直接去项目仓库 (opens new window)。

    组件是使用 uni_modules 的方式引入的,怎么做呢?(先下载 hbuilderX)在插件市场点击【使用 HBuilderX 导入插件】,插件下载成功后,uni_modules 就存在根目录啦。然后, 悄悄地切回 vscode,哈哈,继续开发。

    // 引入
    import lPainter from '@/uni_modules/lime-painter/components/l-painter/l-painter.vue'
    
    1
    2

    注意,如果要溢出打点,可以使用line-clamp: 1;,但别忘了加宽度。

    # 跳转小程序

    这源于今年小程序官方公布的一个新功能,路径为:右上角-工具-生成小程序 URL Scheme,可以生成一个 weixin://协议的 url,然后通过 location.href 的方式直接跳转。

    // jump.js
    // 小程序跳转到首页
    export async function jumpToMini() {
      const isWxMini = await isWxMiniProgram()
    
      if (isWxMini) {
        // 小程序环境
        wx.miniProgram.navigateTo({ url: '/pages/index' })
      } else {
        // 普通h5
        window.location.href = 'weixin://dl/business/?t=xxx'
      }
    }
    
    export function isWxMiniProgram() {
      // 该api依赖于https://res.wx.qq.com/open/js/jweixin-1.3.2.js
      let ua = window.navigator.userAgent.toLowerCase()
      // 判断是否是微信环境
      return new Promise((resolve) => {
        if (ua.match(/MicroMessenger/i) && ua.match(/MicroMessenger/i)[0] === 'micromessenger') {
          let isWx = false
          if (wx && wx.miniProgram && wx.miniProgram.getEnv) {
            wx.miniProgram.getEnv((res) => {
              isWx = res.miniprogram
              resolve(isWx)
            })
          } else {
            resolve(false)
          }
        } else {
          // 非微信环境逻辑
          resolve(false)
        }
      })
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    之所以有上述方案,是因为 iOS 中如果小程序内嵌了 h5,无法通过 location.href 的方式跳转,所以要判断是否小程序环境,如果是,则使用 jssdk 提供的 api 跳转。

    微信内 H5 支持 URL Scheme 跳转小程序了 (opens new window)

    ios 端 web-view 内嵌 h5 页面,无法通过 url scheme 实现跳转 (opens new window)

    这里还遇到一个小插曲,获取到的 wx.miniProgram 始终为 undefined,发现在 uniapp 中,不能直接在 index.html 插入 jssdk。而是在 onLaunch 中动态设置 script 标签的方式 引入。

    // App.vue
    import { importWxJssdk } from '@/utils/jssdk'
      onLaunch() {
          importWxJssdk()
      }
    
    1
    2
    3
    4
    5
    // jssdk.js
    export function importWxJssdk() {
      const script = document.createElement('script')
      script.src = 'https://res.wx.qq.com/open/js/jweixin-1.3.2.js'
      document.body.appendChild(script)
    }
    
    1
    2
    3
    4
    5
    6

    # 封装 vue 指令

    点击节流是常见的需求,比如登录、弹窗等,封装如下:

    /**
     * 使用指令给点击事件节流,默认冷却时间300ms
     * @param Vue
     * @example
     *    <button v-throttle @click="clickIt">点我</button>
     */
    export function addClickThrottle(Vue) {
      Vue.directive('throttle', {
        inserted(el, binding) {
          el.addEventListener('click', () => {
            if (!el.disabled) {
              el.disabled = true
              el.style['pointer-events'] = 'none' // disabled无法禁用点击,使用css控制
              setTimeout(() => {
                el.disabled = false
                el.style['pointer-events'] = ''
              }, binding.value || 300)
            }
          })
        },
      })
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #前端#总结
    上次更新: 2022/01/04, 18:25:35
    企微机器人
    vite踩坑之旅

    ← 企微机器人 vite踩坑之旅→

    最近更新
    01
    提交代码时修改commit消息
    04-09
    02
    如何快速定位bug
    02-20
    03
    云端web项目开发踩坑
    08-25
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 dwfrost | 粤ICP备2021118995号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×