圣诞节活动总结
# 项目复盘
这次圣诞节活动需求有如下特点:
花样多,有许多非常规功能,比如播放音乐,摇一摇,震动,动画等等
时间紧,时间线如下
节点 时间 被通知参与开发 12.13 需求评审 12.13 (设计稿未出)技术调研+搭建工程 12.13-12.15 联调(当天给出接口文档) 12.16 提测 12.16 上线 12.23
此次开发暴露前端一些问题:
- 不能快速选型技术方案,搭建工程
- 工程模板在
vue2+ts
和uniapp
摇摆选择。 - 前者是之前搭建过的项目模板,代码规范和全家桶配置比较成熟,不过 h5 涉及的原生功能需要自己开发 。
- 后者是 uniapp 的 h5 模板,项目组之前有较多的 uniapp 开发经验,大部分可以复用,uni 上 API 丰富。不过这个模板的项目依赖比较老,个人内心对于 uniapp 的工程配置 和目录结构不是很认可。
- 工程模板在
- 开发原生功能经验不足,技术调研花费较多时间,技术细节见下文。
- 没有提前找后端对接口文档。不过本次业务需求较简单,略过。
# 雪花效果
先上最终效果。
代码很简单,是纯 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;
}
}
}
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
图片如下
整个方案是最简单的,拿来即用。之前在 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>
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()
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')
}
}
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')
}
})
},
}
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'
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)
}
})
}
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()
}
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)
}
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)
}
})
},
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22