这篇文章主要介绍了基于vue的移动端web音乐播放器的实现,有着一定的参考价值,现在分享给大家,有需要的朋友可以参考一下
项目GitHub地址: https://github.com/bjw1234/vu...
项目演示地址: http://music.baijiawei.top
学习这个项目花的时间太久了,终于完成啦,算是一个比较庞大和复杂的vue项目了,如果觉得还不错,欢迎star
。
长文预警...
^_^||
// 安装vue脚手架工具 npm install vue-cli -g // 初始化webpack应用 vue init webpack vue-music
// 背景图片 bg-image($url) background-image: url($url + "@2x.png") @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) background-image: url($url + "@3x.png") // 不换行 no-wrap() text-overflow: ellipsis overflow: hidden white-space: nowrap // 扩展点击区域 extend-click() position: relative &:before content: '' position: absolute top: -10px left: -10px right: -10px bottom: -10px
resolve: { extensions: ['.js', '.vue', '.json'], alias: { '@': resolve('src'), 'common': resolve('src/common') } }
fastclick:
处理移动端click事件300毫秒延迟和点透问题。
先执行安装fastclick的命令。
npm install fastclick --save
之后,在main.js中引入,并绑定到body
import FastClick from 'fastclick'; FastClick.attach(document.body);
注意: 当fastclick
和其他的模块点击冲突,导致点击事件不可用时,可以给对应的dom添加needsclick
类来解决。
下载原始的jsonp
模块:
npm install jsonp --save
再次封装:
import originJSONP from 'jsonp'; /** * 做一个简单的jsonp封装 * @param url * @param data * @param option * @return {Promise} */ export default function jsonp (url, data, option) { return new Promise((resolve, reject) => { url = `${url}?${_obj2String(data)}`; originJSONP(url, option, (err, data) => { if (!err) { resolve(data); } else { reject(err); } }); }); }; function _obj2String (obj, arr = [], index = 0) { for (let item in obj) { arr[index++] = [item, obj[item]]; } return new URLSearchParams(arr).toString(); }
注意: 当使用keep-alive
组件时,当切换到其他路由,会调用前组件的deactivated
钩子函数,当切回来时,会调用activated
函数。
注意:
1.better-scroll只处理容器的第一个子元素的滚动。
2.一定得保证子元素超出父元素,这样才能正确的滚动。
初始化:
import BScroll from 'better-scroll'; let wrapper = document.querySelector('.wrapper'); let scroll = new BScroll(wrapper,{ // 配置项 });
.wrapper position: fixed width: 100% top: 88px bottom: 0 .scroll height: 100% overflow: hidden
问题排查(无法滚动原因:)
1.内层容器的高度没有超过外层容器。
2.dom没有渲染完毕就初始化better-scroll
。
3.改变了dom的显隐性,没有对scroll进行重新计算。
针对3:当dom显示出来之后,加20毫秒延时,然后调用refresh
方法。
当在开发模式下,需要使用一些后台接口,为了防止跨域问题,vue-cli
提供了非常强大的http-proxy-middleware
包。可以对我们的请求进行代理。
进入 config/index.js
代码下如下配置即可:
proxyTable: { '/getDescList': { target: 'http://127.0.0.1:7070/desclist', // 后端接口地址 changeOrigin: true, // secure: false, pathRewrite: { '^/getDescList': '/' } } }
marin-left
或者margin-top
是负值:它会将元素在相应的方向进行移动。left
就是左右方向移动,top
就是上下方向移动。也就是会使元素在文档流里的位置发生变化
。
margin-right
或者margin-bottom
是负值:它不会移动该元素(该元素不变化),但会使该元素后面的元素往前移动。也就是说,如果margin-bottom
为负值,那么该元素下面的元素会往上
移动;如果margin-right
为负值,那么该元素右边的元素会往左
移动,从而覆盖该元素。
需求:在歌手页面下需要一个歌手详情页。
export default new Router({ routes:[ { path: '/', component: Singer, children: [ { path: ':id', compoonent: SingerDetail } ] }, ... ] });
当监听到用户点击之后进行路由跳转:
this.$router.push({ path: `singer/${singer.id}` }); // 别忘了在`Singer`页面中: <router-view></router-view>
简单来说:Vuex
解决项目中多个组件之间的数据通信和状态管理。
Vuex将状态管理单独拎出来,应用统一的方式进行处理,采用单向数据流的方式来管理数据。用处负责触发动作(Action
)进而改变对应状态(State
),从而反映到视图(View
)上。
安装:
npm install vuex --save
引入:
import Vuex from 'vuex'; import Vue from 'Vue'; Vue.use(Vuex);
Vuex的组成部分
使用Vuex开发的应用结构应该是这样的:
State
State
负责存储整个应用的状态数据,一般需要在使用的时候在根节点注入store
对象,后期就可以使用this.$store.state
直接获取状态。
import store from './store'; .. new Vue({ el: '#app', store, render: h => h(App) });
那么这个store
又是什么?从哪来的呢?
store
可以理解为一个容器,包含应用中的state
。实例化生成store
的过程是:
const mutations = {...}; const actions = {...}; const state = {...}; // 实例化store对象并导出 export defautl new Vuex.Store({ state, actions, mutations });
Mutations
中文意思是“变化”,利用它可以来更改状态,本质上就是用来处理数据的函数。store.commit(mutationName)
是用来触发一个mutation
的方法。
需要记住的是,定义的mutation必须是同步函数。
const mutations = { changState(state) { // 在这里改变state中的数据 } }; // 可以在组件中这样触发 this.$store.commit('changeState');
Actions
Actions也可以用于改变状态,不过是通过触发mutation
实现的,重要的是可以包含异步操作。
直接触发可以使用this.$store.dispatch(actionName)
方法。
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); // 状态 const state = { singer: {} }; // 跟踪状态的变化 const mutations = { setSinger (state, singer) { state.singer = singer; } }; // 实例化store对象 export default new Vuex.Store({ state, mutations }); // 在singer组件中提交数据 this.$store.commit('setSinger',singer); // 在singer-detail组件中接收数据 let singer = this.$store.state.singer;
在上面的小栗子中,我们把sate
、mutations
等其他一些内容写在了一起,
但是这种方式不适合大型点的项目。最好能将这些内容拎出来,单独作为一个文件来使用。
在src/store目录中新建以下文件:
state.js 用于存储状态信息
const sate = { singer: {} }; export default state;
mutation-types.js 保存一些常量(mutations中函数的函数名)
export const SET_SINGER = 'SET_SINGER';
mutations.js 用于更改状态(state中的数据)
import * as types from './mutation-types'; // 通过这个函数可以传入payload信息 const mutations = { [types.SET_SINGER](state,singer){ state.singer = singer; } }; export default mutations;
getters.js 对状态获取的封装
export const singer = state => state.singer;
actions.js 对mutation进行封装,或者执行一些异步操作
// 暂时没有什么异步操作
index.js store的入口文件
// 入口文件 import Vue from 'vue'; import Vuex from 'vuex'; import state from './state'; import mutations from './mutations'; import * as actions from './actions'; import * as getters from './getters'; import createLogger from 'vuex/dist/logger'; Vue.use(Vuex); // 调试环境下开启严格模式 const debug = process.env.NODE_ENV !== 'production'; // 创建store对象并导出 export default new Vuex.Store({ state, actions, getters, mutations, strict: debug, plugins: debug ? [createLogger()] : [] });
使用:
// main.js中引入 import store from './store';
有了以上内容,那么我们就可以在业务中去使用了:
例如:多组件之间的的数据交互。
需求:singer
组件中需要将用户点击的那个singer对象
传递给组件singer-detail
组件。
singer.vue 组件中:
// 使用这个语法糖 import { mapMutations } from 'vuex'; methods:{ ...mapMutations({ // 将这个函数(setSinger)和mutations中用于修改状态的函数关联起来 setSinger: 'SET_SINGER' }); } // 传参 this.setSinger(singer); // 语法糖的本质 this.$store.commit('setSinger', singer);
singer-detail.vue 组件中:
我们就可以去使用这个数据了,当然也是使用我们的语法糖啦。
import { mapGetters } from 'vuex'; export default { // 使用一个计算属性 computed: { ...mapGetters([ 'singer' // 这个就是getters.js中的那个singer ]); }, created(){ console.log(this.singer); } } // 语法糖的本质: let singer = this.$store.state.singer;
我们一定遇到过这种情况:
需要用JS写CSS动画。但我们又不得不处理前缀的问题。
所以一般是这样写的:
this.$refs.image.style.transform = `scale(${scale})`; this.$refs.image.style.webkitTansform = `scale(${scale})`; ...
那么问题来了,怎样用JS处理这种情况呢?
思路:
检测浏览器的能力。
返回带着前缀的CSS样式。
代码实现:
let elementStyle = document.createElement('p').style; // 得到合适的浏览器前缀 let vendor = (() => { let transformNames = { webkit: 'webkitTransform', Moz: 'MozTransform', O: 'OTransform', ms: 'msTransform', standard: 'transform' }; for (let key in transformNames) { let support = elementStyle[transformNames[key]] !== undefined; if (support) { return key; } } return false; })(); // 对外暴露的方法 export function prefixStyle (style) { if (vendor === false) { return style; } if (vendor === 'standard') { return style; } let result = vendor + style.charAt(0).toUpperCase() + style.substr(1); return result; }
使用案例:
// 导入该模块 import { prefixStyle } from 'common/js/dom'; // 加了合适前缀的CSS属性 const TRANSFORM = prefixStyle('transform'); // 使用该CSS属性 this.$refs.image.style[TRANSFORM] = `scale(${scale})`;
随着触屏设备的普及,w3c为移动端web新增了touch事件。
最基本的touch事件包括4个事件:
touchstart 当在屏幕上按下手指时触发
当用户手指触摸到的触摸屏的时候触发。事件对象的 target
就是 touch
发生位置的那个元素。
touchmove 当在屏幕上移动手指时触发
即使手指移出了 原来的target
元素,但 touchmove
仍然会被一直触发,而且 target
仍然是原来的 target
元素。
touchend 当在屏幕上抬起手指时触发
当用户的手指抬起的时候,会触发 touchend
事件。如果用户的手指从触屏设备的边缘移出了触屏设备,也会触发 touchend
事件。
touchend
事件的 target
也是与 touchstart
的 target
一致,即使已经移出了元素。
touchcancel 当一些更高级别的事件发生的时候(如电话接入或者弹出信息)会取消当前的touch操作,即触发touchcancel。一般会在touchcancel时暂停游戏、存档等操作。
如果你使用了触摸事件,可以调用 event.preventDefault()来阻止鼠标事件被触发。
与移动端相关的interface
主要有三个:
TouchEvent 表示触摸状态发生改变时触发的event
可以通过检查触摸事件的 TouchEvent.type
属性来确定当前事件属于哪种类型。
dom.addEventListener('touchstart',(e) => { // 获取事件类型 let type = e.type; // toch事件发生时那个位置的元素对象 let target = e.target; });
Touch 表示用户和触屏设备之间接触时单独的交互点(a single point of contact)
screenX
、screenY
:触点相对于屏幕左边缘或上边缘的x、y坐标。clientX
、clientY
:触点相对于浏览器viewport左边缘或上边缘的x、y坐标。(不包含滚动距离)
pageX
、pageY
:触点相对于document的左边缘或上边缘的x、y坐标。与client不同的是,包含左边滚动的距离。
target
:触摸开始时的element。
// 获取touchList let touchList = e.changedTouches; // 获取第i个touch对象 let touch = touchList[i]; touch.screenX touch.clientX touch.pageX touch.target ...
TouchList 表示一组touches。当发生多点触摸的时候才用的到。
如果一个用户用三根手指接触屏幕(或者触控板), 与之相关的TouchList
对于每根手指都会生成一个 Touch
对象, 共计 3 个.
可以通过三种方式获取这个对象:
dom.addEventListener('touchstart',(e) => { // 这个 TouchList对象列出了和这个触摸事件对应的那些发生了变化的 Touch 对象 e.changedTouches // 这个TouchList列出了那些 touchstart发生在这个元素,并且还没有离开 touch surface 的touch point(手指) e.targetTouches // 这个 TouchList 列出了事件触发时: touch suface上所有的 touch point。 e.touches });
对于音乐的播放,我们使用了audio
标签,监听它的事件和操作DOM,可以达到对音乐播放、
暂停、进度控制等操作。
<audio ref="audio" :src="currentSongUrl" @canplay="songCanPlay" @error="songError" @ended="songEnd" @timeupdate="updateTime"> </audio>
对audio
进行操作
let audio = this.$refs.audio; // 暂停和播放 audio.pause(); audio.play(); // Audio对象的属性(部分) audio.currentTime // 设置或返回音频中的当前播放位置(以秒计)。 audio.duration // 返回音频的长度(以秒计)。 audio.loop // 设置或返回音频是否应在结束时再次播放。(默认false) audio.volume // 设置或返回音频的音量。[0,1] // Audio对象多媒体事件(Media Events) onerror // 加载发生错误时的回调 ontimeupdate // 当播放位置改变时调用 updateTime(e) { if(this.currentSongReady){ // 获取当前播放的进度 this.currentSongTime=e.traget.currentTime; } } oncanplay // 能够播放时调用 // 通过监听这个事件,设置标志位,这个标志位可以帮助我们 // 防止用户快速切换歌曲引起一些错误。 songCanPlay(){ this.currentSongReady = true; } onended // 到达结尾时调用 onplay、onpause...
1.progress-bar.vue
接收一个percent
参数,用来显示当前播放的一个进度。
2.对于进度条用户手动拖动进度的实现。
<p class="progress-btn" ref="btn" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd"> </p>
思路:主要是通过监听ontouchstart
、ontouchmove
、ontouchend
事件来完成。
// 首先得定义一个`touch`对象 let touch = {}; // 在监听的方法中 touchStart(e){ this.touch.initialized = true; // 获取touch的起始位置 this.touch.startX = e.touches[0].pageX; // 获取整个进度条的宽度 this.touch.barW = xxx; // 获取已经播放的进度 this.touch.offset = xxx; } touchMove(e){ // 判断有无初始化 ... // 获取用户滑动的距离 let deltaX = e.touches[0].pageX - this.touch.startX; let barW = xxx; // 进度条的宽度 - 拖动btn的宽度 let offset = Math.min(Math.max(0, this.touch.offset + detail), barW); // 最后设置btn的位置和progress的进度就OK ... } touchEnd(){ this.touch.initialized = false; // 然后将进度推送出去就好了 this.$emit('percentChange',percent); }
<template> <p class="progress-circle"> <svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg"> <circle class="progress-background" r="50" cx="50" cy="50" fill="transparent"/> <circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent" :stroke-dasharray="dashArray" :stroke-dashoffset="offset"/> </svg> <slot></slot> </p> </template>
通过svg
可以实现各种进度条,有一个问题,怎样去动态的修改它的进度值呢?
这就不能不提 SVG Stroke 属性
stroke 定义一条线,文本或元素轮廓颜色
stroke-width 文本或元素轮廓的厚度
stroke-dasharray 该属性可用于创建虚线
stroke-dashoffset 设置虚线边框的偏移量
OK,知道了以上属性,就足以实现一个可设置进度的SVG进度条了。
思路:stroke-dasharray
适用于创建虚线的,如果这个虚线长度为整个轮廓的周长呢。stroke-dashoffset
可以设置虚线的偏移量,利用这两个属性,我们就可以完成对进度的控制。
且看一个小栗子:
所以,通过父组件传入的percent
,不断地修改stroke-dashoffset
就能达到进度的显示了。
// 全屏显示 document.documentElement.webkitRequestFullScreen(); // 退出全屏 document.webkitExitFullscreen(); // 1.得根据不同的浏览器添加前缀 // 2.程序主动调用不管用,得用户操作才可以(点击按钮)
通过网络接口获取的歌词:
对于歌词的解析,播放是通过一个插件lyric-parser完成的。
这个插件很简单:
1.通过正则把时间和对应的歌词切分出来创建成对象。
2.当调用play
方法时,通过定时器完成歌词的播放,并将对应的行号和歌词通过回调函数传递出去。
当播放的歌词超过5行时,就可以使用封装的scroll
组件完成滚动操作。
if (lineNum > 5) { let elements = this.$refs.lyricLine; this.$refs.lyricScroll.scrollToElement(elements[lineNum - 5], 1000); } else { this.$refs.lyricScroll.scrollTo(0, 0, 1000); }
为什么要使用mixin?
多个组件公用一样的代码,我们可以将这部分抽离出来作为mixin
,只要引入对应的组件中就可以了。
例如下面的mixin
:
import { mapGetters } from 'vuex'; export const playListMixin = { mounted () { this.handlePlayList(this.playList); }, // 当路由对应的页面激活时调用 activated () { this.handlePlayList(this.playList); }, watch: { playList (newPlayList) { this.handlePlayList(newPlayList); } }, computed: { ...mapGetters([ 'playList' ]) }, methods: { // 这个方法需要对应的组件自己去实现,直接调用抛出错误 handlePlayList () { throw new Error('Components must implement handlePlayList method.'); } } };
有了mixin
我们在组件中就可以这样使用了:
import { playListMixin } from 'common/js/mixin'; export default{ mixins: [playListMixin], ... }
在搜索页面,我们需要处理用户的输入,然后向服务器发起请求。
为了不必要的请求、节省流量和提高页面性能,我们都有必要做节流处理。
在搜索框search-box
这个基础组件中:
// 在created钩子中,我们监听用户输入字符串(query)变化,然后将变化后的字符串 // 提交给父组件 // 可以看到在回调函数中,又包了一层debounce函数 created () { this.$watch('query', debounce(() => { this.$emit('queryChange', this.query); }, 500)); }
所以debounce
函数,就是我们的节流函数,这个函数,接收一个函数,返回一个新的函数
function debounce(func,delay){ let timer = null; return function(...args){ if(timer){ clearTimeout(timer); } timer = setTimeout(()=>{ func.apply(this,args); },delay) } } // 测试 function show(){ console.log('hello...'); } var func = debounce(show,3000); // 调用 func(); // 连续调用时,没有超过三秒是不会有任何输出的
语法:
animation: name duration timing-function delay iteration-count direction fill-mode play-state;
animation: 动画名称 执行时间 速度曲线 延时时间 执行次数 动画播放顺序 结束时应用的样式 播放的状态(paused|running)
const __VERSION__ = '1.0.1'; const store = { version: __VERSION__, storage: window.localStorage, session: { storage: window.sessionStorage } }; // 操作store的api const api = { set (key, val) { if (this.disabled) { return false; } if (val === undefined) { return this.remove(key); } this.storage.setItem(key, this.serialize(val)); return val; }, get (key, val) { if (this.disabled) { return false; } let result = this.storage.getItem(key); if (!result) { return val; } return this.deSerialize(result); }, getAll () { if (this.disabled) { return false; } let ret = {}; for (let key in this.storage) { if (this.storage.hasOwnProperty(key)) { ret[key] = this.get(key); } } return ret; }, remove (key) { if (this.disabled) { return false; } this.storage.removeItem(key); }, removeAll () { if (this.disabled) { return false; } this.storage.clear(); }, forEach (cb) { if (this.disabled) { return false; } for (let key in this.storage) { if (this.storage.hasOwnProperty(key)) { cb && cb(key, this.get(key)); } } }, has (key) { if (this.disabled) { return false; } return key === this.get(key); }, serialize (val) { try { return JSON.stringify(val) || undefined; } catch (e) { return undefined; } }, deSerialize (val) { if (typeof val !== 'string') { return undefined; } try { return JSON.parse(val) || undefined; } catch (e) { return undefined; } } }; // 扩展store对象 Object.assign(store, api); Object.assign(store.session, api); // 浏览器能力检测 try { let testKey = 'test_key'; store.set(testKey, testKey); if (store.get(testKey) !== testKey) { store.disabled = true; } store.remove(testKey); } catch (e) { store.disabled = true; } export default store;
为什么需要?
如果开发的App太大的话,就会导致首屏渲染过慢,为了增强用户体验,加快渲染速度,
需要用到懒加载功能。让首屏的内容先加载出来,其他路由下的组件按需加载。
vue官网描述:
异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。
为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。
Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
const AsyncComponent = () => ({ // 需要加载的组件 (应该是一个 `Promise` 对象) component: import('./MyComponent.vue'), // 异步组件加载时使用的组件 loading: LoadingComponent, // 加载失败时使用的组件 error: ErrorComponent, // 展示加载时组件的延时时间。默认值是 200 (毫秒) delay: 200, // 如果提供了超时时间且组件加载也超时了, // 则使用加载失败时使用的组件。默认值是:`Infinity` timeout: 3000 })
注意:如果你希望在 Vue Router 的路由组件中使用上述语法的话,你必须使用 Vue Router 2.4.0+ 版本。
当然为了简单起见:
在router/index.js
路由配置文件中这样加载组件:
// import Recommend from '@/components/recommend/recommend'; const Recommend = () => ({ component: import('@/components/recommend/recommend') });
以上就是本文的全部内容,希望对大家的学习有所帮助,更多相关内容请关注PHP中文网!
相关推荐:
Atas ialah kandungan terperinci 基于vue的移动端web音乐播放器的实现. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!