下载安装新依赖
先添加,在npm install
main.js
import 'babel-polyfill' import Vue from 'vue' import App from './App' import router from './router' import fastclick from 'fastclick' import 'common/stylus/index.styl' Vue.config.productionTip = false // 取消点击300毫秒的延迟 fastclick.attach(document.body) /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>' })
头部栏引用header组件
1:
<template> <div class="m-header"> <div class="icon"></div> <h1 class="text">Chicken Music</h1> <router-link tag="div" class="mine" to="/user"> <i class="icon-mine"></i> </router-link> </div> </template> <script type="text/ecmascript-6"> export default {} </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "~common/stylus/variable" @import "~common/stylus/mixin" .m-header position: relative height: 44px text-align: center color: $color-theme font-size: 0 .icon display: inline-block vertical-align: top margin-top: 6px 30px height: 32px margin-right: 9px bg-image('logo') background-size: 30px 32px .text display: inline-block vertical-align: top line-height: 44px font-size: $font-size-large .mine position: absolute top: 0 right: 0 .icon-mine display: block padding: 12px font-size: 20px color: $color-theme </style>
2:在app.vue
<template> <div id="app"> 3:显式 <m-header></m-header> </div> </template> <script> 1:导入 import MHeader from './components/m-header/m-header' export default { components:{ 2:注册 MHeader } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> </style>
二:导入歌手页面,搜索页面,排行榜,推荐页面
1:先在index.js入口注册这4个组件
import Reacommed from 'components/reacommed/reacommed' import Search from 'components/search/search' import Singer from 'components/singer/singer' import Rank from 'components/rank/rank'
2:配置url
export default new Router({ routes: [ { path: '/reacommed', component:Reacommed }, { path:'/singer', component:Singer }, { path:'/rank', component:Rank }, { path:'/search', component:Search } ] })
import Vue from 'vue' import Router from 'vue-router' import Reacommed from 'components/reacommed/reacommed' import Search from 'components/search/search' import Singer from 'components/singer/singer' import Rank from 'components/rank/rank' Vue.use(Router) export default new Router({ routes: [ { path: '/reacommed', component:Reacommed }, { path:'/singer', component:Singer }, { path:'/rank', component:Rank }, { path:'/search', component:Search } ] })
3:如何引入router实例
import 'babel-polyfill' import Vue from 'vue' import App from './App' // 1这里的router是index。js的实例 import router from './router' import fastclick from 'fastclick' import 'common/stylus/index.styl' Vue.config.productionTip = false // 取消点击300毫秒的延迟 fastclick.attach(document.body) /* eslint-disable no-new */ new Vue({ el: '#app', // 2 router, components: { App }, template: '<App/>' })
注册url
import Vue from 'vue' import Router from 'vue-router' import Recommend from 'components/recommend/recommend' import Search from 'components/search/search' import Singer from 'components/singer/singer' import Rank from 'components/rank/rank' Vue.use(Router) export default new Router({ routes: [ { path: '/recommend', component:Recommend }, { path:'/singer', component:Singer }, { path:'/rank', component:Rank }, { path:'/search', component:Search } ] })
4:显示在App.vue页面
知识点:
router-linnk
a:里面有一个tag属性,控制其显示的为什么标签
如:tag=“a”,即显示为a标签
b:router-link-active
当前某个router-link被激活的时候,会添加样式
5:将导航条添加进去
a:先在app.vue导入进去tab组件
b:注册
c:渲染
<template> <div id="app"> <m-header></m-header> <tab></tab> <router-view></router-view> </div> </template> <script> import MHeader from './components/m-header/m-header' import Tab from './components/tab/tab' export default { components:{ MHeader, Tab } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> </style>
url重定向
redirect
三:在app.vue导入组件
import MHeader from './components/m-header/m-header'
import Tab from './components/tab/tab'
首字母要大写,因为其本质上是class,class书写规范为首字母大写
jsonp
jsonp(url, opts, fn) url (String) url to fetch opts (Object), optional
param : 指定回调函数名
timeout : 超时时间,默认一分钟
prefix : __jp 默认添加前缀
name fn callback (回调函数)
四:抓取qq音乐的数据
XHR:ajax请求
1:可以自己手写
2:引用插件
下载安装jsonp
npm install jsonp
.jsonp是什么?jsonp是目前可以跨域的(基本上标签带有src属性的都是可以不受任何访问限制),且要动态生成script标签在ajax无法跨域的情况下可以使用jsonp进行请求但它跟ajax是不一样的..jsonp利用url链接进行请求发送和调用回调函数(callblack)使用数据。
1:封装一个jsonp方法
用于拼接url
import originJsonp from 'jsonp' // 引用 export default function jsonp(url, data, option) {
// 将url和data对象进行拼接成url拼接 url += (url.indexOf('?') < 0 ? '?' : '&') + param(data) // param 是一个拼接函数将data专门转化成url形式
return new Promise((resolve, reject) => { originJsonp(url, option, (err, data) => { if (!err) { resolve(data) } else { reject(err) } }) }) } export function param(data) { let url = '' for (var k in data) { let value = data[k] !== undefined ? data[k] : '' url += '&' + k + '=' + encodeURIComponent(value) } return url ? url.substring(1) : '' }
Promise对象有2个参数(resolve,reject)
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),
在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),
在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
promise()充当异步操作和回调函数的中介,起到代理的作用
param()data数据对象转化成url格式
function param(data){ let url = ''; //遍历拼接对象 for(var k in data){ //以&a=123=yu=789 这样形式拼接 let value = data[k] !== undefined ?data[k] : ''; url += `&${k}=${encodeURIComponent(value)}`; //es6语法 } return url ? url.substring(1):''; }
在创建的config.js下写入jsonp请求通用公共参数
xport const commonParams = { g_tk: 5381, inCharset: 'utf-8', outCharset: 'utf-8', notice: 0, format: 'jsonp', }
// 设置常量 param export const options = { param: 'jsonpCallback' }
// 设置常量 错误信息 0 是 ok export const ERR_OK = 0
recommend.js文件下写入抓取QQ音乐轮播图需要发送数据
import jsonp from '../common/js/jsonp' //引入自定义封装的jsonp函数 import { commonParams, options } from './config' export function getRecommend() { //轮播图请求地址 const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'; 利用es6对象方法进行浅拷贝将数组对象合并到第一个{}对象中 const data = Object.assign({}, commonParams, { platform: 'h5', uin: 0, needNewCode: 1 }) //调用jsonp方法 进行url拼接 return jsonp(url, data, options) }
.组件页面得到数据
将其js文件引入组件中
import {getRecommend} from 'api/recommend'; import {ERR_OK} from 'api/config'g'
获取数据
_getRecommend(){ //因为return new Promise中有.then表示如果异步成功完成就执行 getRecommend().then(res=>{ if(res.code === ERR_OK){ console.log(res.data.slider) this.recommend = res.data.slider;
//放入组件data中接着传入到轮播组件中使用v-for渲染 } }) }
编写拼接url的方法(getRecommend)
MusicJsonCallback30832226944503405({"code":0,
"subcode":0,"msg":"",
"data":{"total":4,"items":[{"from":2,"status":0,"msg_num":0},
{"from":4,"status":0,"msg_num":0},{"from":5,"status":0,"msg_num":0},
{"from":6,"status":0,"msg_num":0}]}})
从recommend调用getRecommend方法
推荐页面
recommend。vue
_getRecommend() { getRecommend().then((res) => { if(res.code === ERR_OK){ // 轮播图数据 this.recommends = res.data.slider } }) },
手写轮播图组件 base/slider
better-scroll文档**
slider.vue
<!-- 轮播图 --> <template> // slider为最外容器 <div class="slider" ref="slider"> // sliderGroup为内层容器 <div class="slider-group" ref="sliderGroup"> <slot> </slot> </div> </div> </template> <script> // 轮播图组件 import BScroll from 'better-scroll' import {addClass} from 'common/js/dom' export default{ data(){ return { dots:[] } }, props:{ loop:{ // 循环轮播 type:Boolean, default: true }, autoPlay:{ // 自动轮播 type:Boolean, default: true }, interval:{ // 时间间隔 type:Number, default:4000 } }, mounted(){ setTimeout(() =>{ this._setSliderWidth() this._initSlider() this._initDots() },20) }, methods:{ _setSliderWidth(){ this.children = this.$refs.sliderGroup.children let width = 0 let sliderWidth = this.$refs.slider.clientWidth for (let i = 0; i < this.children.length;i++){ let child = this.children[i] addClass(child,'slider-item') child.style.width = sliderWidth + 'px' width += sliderWidth } if(this.loop){ width += 2 * sliderWidth } this.$refs.sliderGroup.style.width = width + 'px' }, _initDots() { this.dots = new Array(this.children.length-2) }, _initSlider(){ this.slider = new BScroll(this.$refs.slider,{ scrollX:true, scrollY:false, momentum:false, snap:true, snapLoop:this.loop, snapThreshold:0.3, snapSpeed:400, click:true } ) } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "~common/stylus/variable" .slider min-height: 1px .slider-group position: relative overflow: hidden white-space: nowrap .slider-item float: left box-sizing: border-box overflow: hidden text-align: center a display: block 100% overflow: hidden text-decoration: none img display: block 100% .dots position: absolute right: 0 left: 0 bottom: 12px text-align: center font-size: 0 .dot display: inline-block margin: 0 4px 8px height: 8px border-radius: 50% background: $color-text-l &.active 20px border-radius: 5px background: $color-text-ll </style>
<div class="slider" ref="slider">---> 最外层容器 <div class="slider-group" ref="sliderGroup">---> 内层容器 sliderGroup轮播图,只能设置宽度,高度固定 1:获取整个列表有多少个元素 this.children = this.$refs.sliderGroup.children 2:父元素的宽度,图片撑开的宽度 let sliderWidth = this.$refs.slider.clientWidth
给轮播图图片添加样式
(手动写的class,耦合性太强)
a、dom。js--->只对dom元素进行操作的方法(通用)
b、再导入dom。js
// 对dom元素操作的组件 import {addClass} from 'common/js/dom'
c、将slider-item样式添加进去
addClass(child,'slider-item')
计算部分
// 子容器的宽度 要等于父容器的宽度 需要添加单位'px'
child.style.width = sliderWidth + 'px'
// 总宽度需要+父容器的宽度
width += sliderWidth
loop:{
// 循环轮播
type:Boolean,
default: true
},
// 保证其能进行循环切换,需克隆2个sliderWidth-->即宽度需要加2倍sliderWidth
if(this.loop && !isResize){
width += 2 * sliderWidth
}
最后设置宽度
this.$refs.sliderGroup.style.width = width + 'px'
问题:当后面方法需要获取数据时,而获取数据的方法还未执行完毕
在recommend。vue进行判断数据长度
<div v-if='recommends.length' class="slider-wrapper"> <slider> <div v-for="item in recommends"> <a :href="item.linkUrl"> <img :src="item.picUrl"> </a> </div> </slider> </div>
recommend.vue推荐页面
<!-- 推荐页面 --> <template> <div class="recommend"> <div class="recommend-content"> <!-- 轮播图 --> <div v-if='recommends.length' class="slider-wrapper"> <slider> <div v-for="item in recommends"> <a :href="item.linkUrl"> <img :src="item.picUrl"> </a> </div> </slider> </div> <div class="recommend-list"> <h1 class="list-title">热门歌单推荐</h1> </div> <ul> </ul> </div> </div> </template> <script> // 导入轮播图组件 import Slider from 'base/slider/slider' // 导入获取轮播数据的方法 import {getRecommend} from 'api/recommend' // 语义化下面的err_ok import {ERR_OK} from 'api/config' // 获取数据created export default { data() { // 定义轮播图数据 return{ recommends:[] } }, // 钩子函数 created() { this._getRecommend() }, methods: { _getRecommend() { getRecommend().then((res) => { if(res.code === ERR_OK){ // 轮播图数据 this.recommends = res.data.slider } }) } }, components:{ Slider, } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "~common/stylus/variable" .recommend position: fixed 100% top: 88px bottom: 0 .recommend-content height: 100% overflow: hidden .slider-wrapper position: relative 100% overflow: hidden .recommend-list .list-title height: 65px line-height: 65px text-align: center font-size: $font-size-medium color: $color-theme .item display: flex box-sizing: border-box align-items: center padding: 0 20px 20px 20px .icon flex: 0 0 60px 60px padding-right: 20px .text display: flex flex-direction: column justify-content: center flex: 1 line-height: 20px overflow: hidden font-size: $font-size-medium .name margin-bottom: 10px color: $color-text .desc color: $color-text-d .loading-container position: absolute 100% top: 50% transform: translateY(-50%) </style>
轮播图下的圆圈
<!-- 轮播图 --> <template> <div class="slider" ref="slider"> <div class="slider-group" ref="sliderGroup"> <slot> </slot> </div> <!-- 圆圈 --> <div class="dots"> <span class="dot" v-for = 'item in dots'></span> </div> </div> </template> <script> // 轮播图组件 import BScroll from 'better-scroll' import {addClass} from 'common/js/dom' export default{ data(){ return { dots:[] } }, props:{ loop:{ // 循环轮播 type:Boolean, default: true }, autoPlay:{ // 自动轮播 type:Boolean, default: true }, interval:{ // 时间间隔 type:Number, default:4000 } }, mounted(){ setTimeout(() =>{ this._setSliderWidth() this._initSlider() this._initDots() },20) }, methods:{ _setSliderWidth(){ this.children = this.$refs.sliderGroup.children let width = 0 let sliderWidth = this.$refs.slider.clientWidth for (let i = 0; i < this.children.length;i++){ let child = this.children[i] addClass(child,'slider-item') child.style.width = sliderWidth + 'px' width += sliderWidth } if(this.loop){ width += 2 * sliderWidth } this.$refs.sliderGroup.style.width = width + 'px' }, _initDots() { this.dots = new Array(this.children.length-2) }, _initSlider(){ this.slider = new BScroll(this.$refs.slider,{ scrollX:true, scrollY:false, momentum:false, snap:true, snapLoop:this.loop, snapThreshold:0.3, snapSpeed:400, click:true } ) } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "~common/stylus/variable" .slider min-height: 1px .slider-group position: relative overflow: hidden white-space: nowrap .slider-item float: left box-sizing: border-box overflow: hidden text-align: center a display: block 100% overflow: hidden text-decoration: none img display: block 100% .dots position: absolute right: 0 left: 0 bottom: 12px text-align: center font-size: 0 .dot display: inline-block margin: 0 4px 8px height: 8px border-radius: 50% background: $color-text-l &.active 20px border-radius: 5px background: $color-text-ll </style>
将轮播图当前页与小圆圈绑定样式
<div class="dots">
<span class="dot" v-for = '(item,index) in dots' :class="{active: currentPageIndex === index}">
</span> </div>
index--->索引当前第几个元素
添加样式active
:class="{active: currentPageIndex === index}">
currentPageIndex:标识当前页数
需要先在data() 方法里面初始化currentPageIndex
export default{ data(){ return { dots:[], // 标识当前页数 currentPageIndex: 0 } },
到索引的页面就添加active类
当前对象。也就是指.dot
当dot是active的时候,就添加样式
维护currentPageIndex
currentPageIndex什么时候切换的?如何将滚动到的页面与currentPageIndex结合? 当初始化slider的时候,绑定事件 // 当滚动到下一张会触发scrollEnd事件 scrollEnd事件会派发一个回调函数 this.slider.on('scrollEnd', ()=>{ // 获取当前 getCurrentPage()为slider的方法 返回的对象有个pageX方法 pageX--->当前第几个元素 let pageIndex = this.slider.getCurrentPage().pageX if (this.loop) { pageIndex -= 1 //再循环模式下,会默认在第一个元素添加一个拷贝 所以要减1 } this.currentPageIndex = pageIndex })
这里我们用到了 Vue 的特殊元素—— slot 插槽,它可以满足我们灵活定制列表 DOM 结构的需求。接下来我们来看看 JS 部分: <script type="text/ecmascript-6"> import BScroll from 'better-scroll' export default { props: { /** * 1 滚动的时候会派发scroll事件,会截流。 * 2 滚动的时候实时派发scroll事件,不会截流。 * 3 除了实时派发scroll事件,在swipe的情况下仍然能实时派发scroll事件 */ probeType: { type: Number, default: 1 }, /** * 点击列表是否派发click事件 */ click: { type: Boolean, default: true }, /** * 是否开启横向滚动 */ scrollX: { type: Boolean, default: false }, /** * 是否派发滚动事件 */ listenScroll: { type: Boolean, default: false }, /** * 列表的数据 */ data: { type: Array, default: null }, /** * 是否派发滚动到底部的事件,用于上拉加载 */ pullup: { type: Boolean, default: false }, /** * 是否派发顶部下拉的事件,用于下拉刷新 */ pulldown: { type: Boolean, default: false }, /** * 是否派发列表滚动开始的事件 */ beforeScroll: { type: Boolean, default: false }, /** * 当数据更新后,刷新scroll的延时。 */ refreshDelay: { type: Number, default: 20 } }, mounted() { // 保证在DOM渲染完毕后初始化better-scroll setTimeout(() => { this._initScroll() }, 20) }, methods: { _initScroll() { if (!this.$refs.wrapper) { return } // better-scroll的初始化 this.scroll = new BScroll(this.$refs.wrapper, { probeType: this.probeType, click: this.click, scrollX: this.scrollX }) // 是否派发滚动事件 if (this.listenScroll) { let me = this this.scroll.on('scroll', (pos) => { me.$emit('scroll', pos) }) } // 是否派发滚动到底部事件,用于上拉加载 if (this.pullup) { this.scroll.on('scrollEnd', () => { // 滚动到底部 if (this.scroll.y <= (this.scroll.maxScrollY + 50)) { this.$emit('scrollToEnd') } }) } // 是否派发顶部下拉事件,用于下拉刷新 if (this.pulldown) { this.scroll.on('touchend', (pos) => { // 下拉动作 if (pos.y > 50) { this.$emit('pulldown') } }) } // 是否派发列表滚动开始的事件 if (this.beforeScroll) { this.scroll.on('beforeScrollStart', () => { this.$emit('beforeScroll') }) } }, disable() { // 代理better-scroll的disable方法 this.scroll && this.scroll.disable() }, enable() { // 代理better-scroll的enable方法 this.scroll && this.scroll.enable() }, refresh() { // 代理better-scroll的refresh方法 this.scroll && this.scroll.refresh() }, scrollTo() { // 代理better-scroll的scrollTo方法 this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments) }, scrollToElement() { // 代理better-scroll的scrollToElement方法 this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments) } }, watch: { // 监听数据的变化,延时refreshDelay时间后调用refresh方法重新计算,保证滚动效果正常 data() { setTimeout(() => { this.refresh() }, this.refreshDelay) } } } </script>
JS 部分实际上就是对 better-scroll 做一层 Vue 的封装,通过 props 的形式,
把一些对 better-scroll 定制化的控制权交给父组件;
通过 methods 暴露的一些方法对 better-scroll 的方法做一层代理;
通过 watch 传入的 data,当 data 发生改变的时候,
在适当的时机调用 refresh 方法重新计算 better-scroll 确保滚动效果正常,
这里之所以要有一个 refreshDelay 的设置是考虑到如果我们对列表操作用到了 transition-group 做动画效果
,那么 DOM 的渲染完毕时间就是在动画完成之后。 有了这一层 scroll 组件的封装,我们来修改刚刚最复杂的代码(假设我们已经全局注册了 scroll 组件)。 <template> <scroll class="wrapper" :data="data" :pulldown="pulldown" @pulldown="loadData"> <ul class="content"> <li v-for="item in data">{{item}}</li> </ul> <div class="loading-wrapper"></div> </scroll> </template> <script> import BScroll from 'better-scroll' export default { data() { return { data: [], pulldown: true } }, created() { this.loadData() }, methods: { loadData() { requestData().then((res) => { this.data = res.data.concat(this.data) }) } } } </script> 可以很明显的看到我们的 JS 部分精简了非常多的代码,没有对 better-scroll 再做命令式的操作了,
同时把数据请求和 better-scroll 也做了剥离,父组件只需要把数据 data 通过 prop 传给 scroll 组件,
就可以保证 scroll 组件的滚动效果。
同时,如果想实现下拉刷新的功能,只需要通过 prop 把 pulldown 设置为 true,
并且监听 pulldown 的事件去做一些数据获取并更新的动作即可,整个逻辑也是非常清晰的。
动态的给子元素设置宽度,当能滑动或者自动轮播的时候,子元素的宽度乘以2,用来切换,因为轮播图想无缝滑动,
必须将第一张图放到最后一个位置,最后一张图放到第一张的位置前面,假如有五个轮播图的话,
事实上,获取的宽度是七张图的宽度。
自动播放
slider.vue
props:{ loop:{ // 循环轮播 type:Boolean, default: true }, autoPlay:{ // 自动轮播 ype:Boolean, default: true
优化:
当屏幕宽度变化的时候,图片错位,而宽度是由sliderWidth控制的
方法: 监听Windows的resizer事件,重新渲染sliderWidth
//监听窗口改变事件 window.addEventListener('resize', () => { if (!this.slider){
// 初始化的时候直接返回 return } // 重新调用计算宽度的方法 this._setSliderWidth(true) }) },
window窗口改变事件:resize
addEventListener() 方法
用于向指定元素添加事件句柄
element.addEventListener(event, function, useCapture)
参数: event: 必须。字符串,指定事件名。 注意: 不要使用 "on" 前缀。 例如,使用 "click" ,而不是使用 "onclick" function 必须。指定要事件触发时执行的函数。 当事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。例如, "click" 事件属于 MouseEvent(鼠标事件) 对象。 useCapture 可选。布尔值,指定事件是否在捕获或冒泡阶段执行。 可能值: true - 事件句柄在捕获阶段执行 false- false- 默认。事件句柄在冒泡阶段执行
添加标志位,重置过的设置为true
methods:{
// isResize标志位判读是不是重置过来的 _setSliderWidth(isResize){ this.children = this.$refs.sliderGroup.children let width = 0 let sliderWidth = this.$refs.slider.clientWidth for (let i = 0; i < this.children.length;i++){ let child = this.children[i] addClass(child,'slider-item') child.style.width = sliderWidth + 'px' width += sliderWidth }
// && !isResize if(this.loop && !isResize){
// 刚开始进来只需要加一次宽度 width += 2 * sliderWidth } this.$refs.sliderGroup.style.width = width + 'px' },
知识点
1:slot---> 插槽
2:使用better-scroll
3:created和mounted
created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。