** jsonp的实现原理**
jsonp发送的并不是ajax请求,而是动态创建的script标签,script表签没有同源策略的,是没有限制,可以跨域的,创建的script标签把src指向我们请求正式的服务端地址
这个地址和我们的ajax的地址有什么不同
那是因为在url这个地址有一个参数通常会叫callback = a(比如=a),这样服务端就会解析这个url 然后它带一个callback = a这样的参数,它就会在返回的数据里调用a然后去包裹一个方法,包裹一段数据,然后去执行,相当于在前端执行a这个方法,那前端没有a这个方法,所以在发送请求之前,也就是通过script的src这个url之前我们要去在window上注册这样一个方法,这样的话,服务端返回这个a()这个方法执行的时候,就可以在window上定义的这个方法中获得这个数据
https://blog.youkuaiyun.com/inite/article/details/80333130
jsonp的promise封装
import originJSONP from 'jsonp'
// data:通常传给服务端的是一个地址 url往往是带一些参数,但是originJSONP这个库是不支持传入一个object 一个data的,需要你先吧这个url拼好,然后再去调用这个originJSON库,实际上,当我们使用的时候,希望这个url是比较纯净的地址,所有的query都通过这个data去把它拼到这个url上,这样的话我们调用就更加方便
export default function jsonp(url, data, option) {
// 实际上url有可能会有?,判断如果没有?就拼接一个,如果有?就用&连接 [url拼接的方式]
url += (url.indexOf('?') < 0 ? '?' : '&') + param(data)
return new Promise((resolve,reject) => {
originJSONP(url, option, (data,err) => {
if(!err) {
resolve(data)
}else{
reject(err)
}
})
})
}
//jsonp的函数里传入了data的参数,需要把data的json对象拼到url参数里
function param(data) {
let url = ''
for(var k in data) {
let value = data[k]!== undefined ? data[k] : '' //判断如果data有值就传data[k]否则传一个空给后端
url += `&${k}=${encodeURIComponent(value)}` // 拼接url
}
return url?url.substring(1):'' // 如果这个url有data要把第一个&符号去掉(substring() 从字符串中提取一些字符) 如果data什么都没有就返回一个空
}
总结
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
Object.assign(target, source1, source2)
encodeURIComponent(URIstring)函数可把字符串作为 URI 组件进行编码。 URIstring 必需。一个字符串,含有 URI 组件或其他要编码的文本。
substring()方法用于提取字符串中介于两个指定下标之间的字符。
stringObject.substring(start,stop)包括 start 处的字符,但不包括 stop 处的字符。不接受负的参数。
jsonp抓取qq音乐的数据
1、npm install jsonp
2、jsonp 原理内容 (上述内容创建jsonp.js)
3、创建confiig.js文件
export const commonParams = {
g_tk: 5381,
inCharset: 'utf-8',
outCharset: 'utf-8',
notice: 0,
format: 'jsonp'
}
export const options = {
param: 'jsonpCallback'
}
export const ERR_OK = 0
4、创建recommend.js文件
import jsonp from 'assets/js/jsonp'
import {commonParams, options} from 'src/api/config'
export function getRecommend() {
const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'
const data = Object.assign({}, commonParams, {
platform: 'h5',
uin: 0,
needNewCode: 1
})
return jsonp(url, data, options)
}
5、在组件中使用
import {getRecommend} from 'src/api/recommend'
import {ERR_OK} from 'src/api/config'
export default {
name: 'Recommend',
data() {
return {
}
},
created() {
this._getRecommend()
},
methods: {
_getRecommend() {
getRecommend().then((res) => {
if (res.code === ERR_OK) {
console.log(res.data.slider)
}
})
.catch((err) => {
console.log(err)
})
}
}
}
banner运用better-scroll时初始化时不能滚动
better-scroll初始化时不能滚动,甚至初始化时报错,其原因主要是因为在初始化的时候,这个组建没有真正的被渲染,或者是高度或者宽度没有计算不正确,所以要先保证渲染实际是正确的
'better-scroll’利用的原理就是:wrapper为父容器,具有固定高度;content为父容器的第一个子元素;当content的高度超过wrapper时,就可以滚动。
安装 “better-scroll”: “^0.1.15”,
①轮播图的功能有:循环轮播,自动轮播,轮播间隔。
实现水平轮播最重要的是计算轮播宽度
//引用dom.js 封装addClass
import {addClass} from 'src/assets/js/dom'
mounted () {
setTimeout(() => {
this._setSliderWidth()
this.initSlider()
}, 20)
},
methods: {
_setSliderWidth() {
this.children = this.$refs.sliderGroup.children
let width = 0
let silderWidth = this.$refs.slider.clientWidth
for (let i = 0; i < this.children.length; i++) {
// 先获取每个子元素
let child = this.children[i]
addClass(child, 'slider-item') //每一个子元素添加className
child.style.width = silderWidth + 'px'
width += silderWidth
}
if (this.loop) {
width += 2 * silderWidth
}
this.$refs.sliderGroup.style.width = width + 'px'
}
宽度定义好以后,就可以初始化滚动了
_initSlider() {
this.slider = new BScroll(this.$refs.slider, {
scrollX: true,//横向滚动
scrollY: false,//禁止纵向滚动
momentum: false,//禁止惯性运动
snap: true,
snapLoop: this.loop,
snapThreshold: 0.3,
snapSpeed: 400
})
dom.js 封装的addClass
export function addClass(el, className) {
if (hasClass(el, className)) {
return
}
let newClass = el.className.split(' ') // split将获取的这个className拆成一个数组split()方法用于把一个字符串分割成字符串数组。 比如
newClass.push(className) // 再把他添加进去
el.className = newClass.join(' ') // join()方法用于把数组中的所有元素放入一个字符串。
}
// 判断是否有这个class
export function hasClass(el, className) {
let reg = new RegExp('(^|\\s)' + className + '(\\s|$)') // 创建一个正则 class开头,或者是空白字符 \\因为是字符串所以要转义一下
return reg.test(el.className) // test()方法用于检测一个字符串是否匹配某个模式,如果满足这个条件说明有这个className
}
注意当执行mounted 钩子的时候,slot里插槽里内容会不会有,slot的内容在re’commed.vue 里的created钩子里获取的,是一个异步的过程,在服务器取数据会有几毫秒的延迟,为了确保slot里是有内容的要用v-if判断
<div class="slider-content" v-if="recommends.length">
<slider>
<div
v-for="item in recommends"
:key="item.id"
>
<a :href="item.linkUrl">
<img :src="item.picUrl" alt="">
</a>
</div>
</slider>
</div>
在获取歌单数据的时候,我们发现用jsonp获取接口的时候会报错,这是为什么呢?
借鉴:https://blog.youkuaiyun.com/sunnyjingqi/article/details/89404538
原因是qq音乐在请求头里面加了authority和refer等 ,如果我们通过jsonp实现跨域来请求数据的话 是根本不能够修改请求头的 ,如果要使用axios直接进行跨域访问是不可以的,这时候我们可以进行后端接口代理 ,客户端请求服务端的数据是存在跨域问题的,而服务器和服务器之间可以相互请求数据,是没有跨域的概念(如果服务器没有设置禁止跨域的权限问题),也就是说,我们可以配置一个代理的服务器可以请求另一个服务器中的数据,然后把请求出来的数据返回到我们的代理服务器中,代理服务器再返回数据给我们的客户端,这样我们就可以实现跨域访问数据啦。
在HTTP请求头里referer是代表这个请求是请哪个URL过来的
origin是在HTML5中跨域操作所引入的,当一个链接或者XMLHttpRequest去请求跨域操作,浏览器事实上的确向目标服务器发起了连接请求,并且携带这origin
host在视频里存在歌单的请求头里,但是现在发现没出现,可能隐藏了,但是我在反向代理ProxyTable里设置了这个参数是同样可以请求成功的。这个参数表示了客户端指定了自己想访问的服务器地址,只要我们使用反向代理向改服务器发送相同的请求头,就可以成功抓取数据到数据,因为目标服务器无法区分是否来自它本身服务器发送的请求。
- npm install axios -S
- build/webpack.dev.conf.js文件
const axios = require('axios')
const express = require('express')
const app = express()
const apiRoutes = express.Router() //后端路由
app.use('/api', apiRoutes)
- // devServer 里添加
before(app){
app.get('/api/getDiscList', function(req,res) {
var url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg'
axios.get(url, {
headers: {
referer: 'https://c.y.qq.com',
host: 'c.y.qq.com'
},
params: req.query // 前端传过来的数据
})
.then((response) => {
res.json(response.data)
})
.catch((e) => {
console.log(e)
})
})
}
- 在api里的js文件里:将方法里的url替换成步骤2里自定义的接口,即 ‘/api/getDiscList’,再通过axios获取返回的数据。
export function getDiscList() {
const url = '/api/getDiscList'
const data = Object.assign({}, commonParams, {
platform: 'yqq',
hostUin: 0,
sin: 0,
ein: 29,
sortId: 5,
needNewCode: 0,
categoryId: 10000000,
rnd: Math.random(),
format: 'json'
})
return axios.get(url, {
params: data
})
.then((res) => {
return Promise.resolve(res.data)
})
}
记住params这个名字千万不能写错,我因为把它写成了param,导致请求参数都没有传过去,找问题的能力太差,最终在header里面发现了,回想起教程视频里老师提醒过这个。
(大家很容易查到:json和jsonp的区别,json是一种格式,jsonp是一种请求跨域资源的方式。这里就不做详细解释了。)
跨域:是指浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的,是浏览器施加的安全限制。在跨域情况下,XMLHTTPRequest是不能发送异步请求的。
所谓同源是指域名、协议、端口均相同。
那么,同是跨域方法,为什么轮播图的请求可以用jsonp的方式,而歌单的请求要使用反向代理,两个都是跨域方法。
比较两个请求jsonp和proxyTable反向代理的异同:
jsonp原理:
反向代理:本方法是在自己的浏览器创建一个服务器,然后让自己的服务器去请求目标服务器。而且跨域是针对JavaScript来说的,JavaScript 是插入HTML页面后在浏览器上执行的脚本。服务器之间是可以随便请求数据而不受限制的。我们通过自己创建的服务器去请求目标服务器,然后在从我们客户端去请求我们自己创建的服务器,这就不存在跨域了。
5. 组件里的调用
_getDiscList() {
getDiscList().then((res) => {
if (res.code === ERR_OK) {
console.log(res)
}
})
}
banner的图片还未加载完毕时,歌单推荐的better-scroll上拉到最底部后不能展示全部的内容
并监听loadImage
不利用jasonp用反向代理完成数据抓取
config文件下的index.js 配置proxyTable
使用
import axios from 'axios'
// import {commonParams} from 'src/api/config'
export function getSingerList() {
const url = '/qqmusic/v8/fcg-bin/v8.fcg'
const data = {
channel: 'singer',
page: 'list',
key: 'all_all_all',
pagesize: 100,
pagenum: 1,
hostUin: 0,
platform: 'yqq',
g_tk: 5381,
loginUin: '0',
format: 'json',
inCharset: 'utf8',
outCharset: 'utf-8',
notice: 0,
needNewCode: 0
}
return axios.get(url, {
params: data
})
.then((res) => {
return Promise.resolve(res.data)
})
}
歌手列表与右侧a-zA-Z之间的联动
联动的思路:
如果想要达到左边和右边的联动,首先需要实时的知道他的滚动位置,然后根据他的滚动位置来算他当前的滚动位置是落在歌手列表哪个group的区间,如果算到落到的区间以后,我们就知道右侧对应的哪个索引,然后哪个区间索引的高亮就显示
在vue中主要用watch观测这个变化然后配合scroll,实时派发出scrollY去观测Y值得变化,观测到y值得变化后,就去计算currentIndex,然后根据currentIndex vue中index的映射(:class)等于当前的索引时,就给他一个active的样式,让对应的索引高亮
1、
<template>
<scroll class="listview"
:data="data"
ref="listview"
:listenScroll = 'listenScroll'
:probeType="probeType"
@scroll="scroll"
>
<ul>
<li v-for="(group, index) in data"
class="list-group"
:key="index"
ref="listGroup"
>
<h2 class="list-group-title">{{group.title}}</h2>
<ul>
<li
v-for="item in group.items"
:key="item.id"
class="list-group-item"
>
<img v-lazy="item.avatar" class="avatar" alt="">
<span class="name">{{item.name}}</span>
</li>
</ul>
</li>
</ul>
<div class="list-shortcut"
@touchstart="onShortcutTouchStart"
@touchmove.stop.prevent="onShortcutTouchmove"
>
<ul>
<li
class="item"
v-for="(item, index) in shortcutList"
:key="index"
:data-index="index"
:class="{'current': currentIndex === index}"
>
{{item}}
</li>
</ul>
</div>
<div class="list-fixed" v-show="fixedTitle" ref="fixed">
<h1 class="fixed-title">{{fixedTitle}}</h1>
</div>
<div v-show="!data.length" class="loading-container">
<loading></loading>
</div>
</scroll>
</template>
<script>
import Scroll from 'src/base/scroll/scroll'
import {getData} from 'src/assets/js/dom'
import Loading from 'src/base/loading/loading'
const ANCHOR_HEIGHT = 18
const TITLE_HEIGHT = 30
export default {
data() {
return {
scrollY: -1,
currentIndex: 0,
diff: -1 // 表示区块上限和当前滚动位置的一个差值
}
},
// 在vue里面不管是data还是props里面的东西都会被vue添加一个getter和setter,它会去观测props data以及computed里面值的变化,如果变化都会做监听,主要是为了与dom做数据绑定
props: {
data: {
type: Array,
default: null
}
},
created() {
this.touch = {} // 为了让touchstar和touchmove两个函数间共享数据
this.listenScroll = true // 监听scroll
this.listHeight = []
this.probeType = 3
},
watch: {
data() {
setTimeout(() => {
this._calculateHeight()
}, 20)
},
scrollY(newY) { // 观测scrollY变化
const listHeight = this.listHeight
// 当滚到顶部,newY>0
if (newY > 0) {
this.currentIndex = 0
return
}
// 中间部分滚动
for (let i = 0; i < listHeight.length - 1; i++) {
let height1 = listHeight[i]
let height2 = listHeight[i + 1]
// 如果他落在了height1和height2的区间内
if (-newY >= height1 && -newY < height2) {
this.currentIndex = i
this.diff = height2 + newY // 得到fixed title上边界距顶部的偏移距离
console.log(this.currentIndex)
return
}
}
// 当滚动到底部,且-newY大于最后一个元素的上限
// currentIndex 比listHeight中的height多一个, 比元素多2个
this.currentIndex = listHeight.length - 2
},
diff(newVal) {
let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0
if (this.fixedTop === fixedTop) {
return
}
this.fixedTop = fixedTop
this.$refs.fixed.style.transform = `translate3d(0, ${fixedTop}px,0)`
}
},
components: {
Scroll,
Loading
},
computed: {
shortcutList() {
return this.data.map((group) => {
return group.title.substr(0, 1)
})
},
fixedTitle() {
if (this.scrollY > 0) { // 边界条件判断
return ''
}
return this.data[this.currentIndex] ? this.data[this.currentIndex].title : ''
}
},
methods: {
onShortcutTouchStart(e) {
// 获取data-index的值 index 得到的是字符串
let anchorIndex = getData(e.target, 'index')
// console.log(anchorIndex)
let fistTouch = e.touches[0] // 获取手指触碰的第一个位置
this.touch.y1 = fistTouch.pageY // 记录一个第一次touch时pageY的值
// 给touch初始化记录一个当前的anchorIndex,你当前点击的index是多少
this.touch.anchorIndex = anchorIndex
this._scrollTo(anchorIndex)
},
onShortcutTouchmove(e) {
let fistTouch = e.touches[0] // move touch时的第一个位置
this.touch.y2 = fistTouch.pageY // 同样获取并记录到第一次touchmove时 pageY的值
let delta = Math.floor((this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT) // 计算y轴上面的一个偏移,并且知道偏移了几个delta(锚点)
// move的时候又可以定义this.touch.anchorIndex获取的是个字符串
let anchorIndex = parseInt(this.touch.anchorIndex) + delta
// console.log(anchorIndex)
this._scrollTo(anchorIndex)
},
scroll(pos) {
this.scrollY = pos.y // 子元素传递的方法,父元素监听这个方法,实时的获取scrollY
},
_scrollTo(index) {
// 处理index边界
// 点击边界的情况
if (!index && index !== 0) {
return
}
// 拖动到边界的情况
if (index < 0) {
index = 0
} else if (index > this.listHeight.length - 2) {
index = this.listHeight.length - 2
}
this.scrollY = -this.listHeight[index] // 点击右侧出现高亮
this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0) // 列表滚动定位
},
_calculateHeight() { // 计算每个group的高度
this.listHeight = [] // 每次重新计算每个group高度时,恢复初始值
const list = this.$refs.listGroup
let height = 0 // 初始位置的height为0
this.listHeight.push(height)
for (let i = 0; i < list.length; i++) {
let item = list[i] // 得到每一个group的元素
height += item.clientHeight // DOM元素可以用clientHeight获取元素高度
this.listHeight.push(height) // 得到每一个元素对应的height
}
}
}
}
</script>
2、
3、
<template>
<div ref="wrapper">
<slot></slot>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
name: 'Scroll',
props: {
probeType: { // 控制better-scroll的probeType
type: Number,
default: 1
},
click: {
type: Boolean,
default: true
},
data: {
type: Array,
default: null
},
listenScroll: { // 监听scroll滚动
type: Boolean,
default: false
}
},
mounted() {
setTimeout(() => {
this._initScroll()
}, 20)
},
methods: {
_initScroll() {
if (!this.$refs.wrapper) {
return
}
this.scroll = new BScroll(this.$refs.wrapper, {
probeType: this.probeType,
click: this.click
})
if (this.listenScroll) {
let that = this
this.scroll.on('scroll', (pos) => {
that.$emit('scroll', pos) // 这个this指向的是scroll所以要重新声明一个变量保存this,这样使用的时候this指向就是vue实例的this
})
}
},
// better scroll 方法代理
enable() {
// 启用better-scroll默认开启
this.scroll && this.scroll.enable()
},
disable() {
// 禁用better-scroll, 如果不加,scroll的高度会高于内容的高度
this.scroll && this.scroll.disable()
},
refresh() {
this.scroll && this.scroll.refresh()
},
// 滚动到指定的位置;这里使用apply 将传入的参数,传入到this.scrollTo()
scrollTo() {
this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
},
// 滚动到指定的目标元素
scrollToElement() {
this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
}
},
watch: {
data() {
setTimeout(() => {
this.refresh()
}, 20)
}
}
}
</script>
vuex
把所有组件的所有状态、数据都存储在统一的内存空间去管理state里,state里的数据可以方便的映射到vue的组件上,去渲染组件,当组件的数据发生变化的时候,它通过Dispatch方法,触发Actions,Actions可以做一些异步的操作比如:后端的一些交互,之后它Commit,Mutations (注意:也可以直接在vue组件中直接commit一个Mutations),commit,Mutations是唯一一个可以修改state的途径,其他任何方式修改都是不允许的。vuex设计的目的是让state状态修改可以预测,当state被修改后,他又会反映到vue组件上,这样就实现一个闭环
使用场景
1、多个组件的状态共享(应用比较复杂,其中一些数据是被组件共享的,而这些组件是兄弟组件,甚至是关联度比较低的组件)
2、解决路由间的复杂的数据传递(一些路由跳转的场景,传递的参数很复杂)
歌单详情下拉图片的放大缩小、以及上拉置顶
<template>
<div class="music-list">
<div class="back">
<i class="icon-back"></i>
</div>
<h1 class="title" v-html="title"></h1>
<div class="bg-image" :style="bgStyle" ref="bgImage">
<div class="filter" ref="filter"></div>
</div>
<div class="bg-layer" ref="bgLayer"></div>
<scroll
@scroll="scroll"
:probe-type="probeType"
:listen-scroll="listenScroll"
:data="songs"
class="list"
ref="list">
<div class="song-list-wrapper">
<song-list :songs="songs"></song-list>
</div>
</scroll>
<div class="loading-container" v-show="!songs.length">
<loading></loading>
</div>
</div>
</template>
<script>
import Scroll from 'src/base/scroll/scroll'
import SongList from 'src/base/song-list/song-list'
import Loading from 'src/base/loading/loading'
import {prefixStyle} from 'src/assets/js/dom'
const transform = prefixStyle('transform')
const backdrop = prefixStyle('background-filter')
const LIXIT_HEIGHT = 40
export default {
data() {
return {
scrollY: 0
}
},
props: {
bgImage: {
type: String,
default: null
},
songs: {
type: Array,
default: null
},
title: {
type: String,
default: null
}
},
computed: {
bgStyle() {
return `background-image:url(${this.bgImage})`
}
},
components: {
Scroll,
SongList,
Loading
},
mounted() {
// $el获取他的一个dom
this.imageHeight = this.$refs.bgImage.clientHeight
this.minTranslateY = -this.imageHeight + LIXIT_HEIGHT
this.$refs.list.$el.style.top = `${this.imageHeight}px`
},
created() {
this.probeType = 3
this.listenScroll = true
},
methods: {
scroll(pos) {
this.scrollY = pos.y
}
},
watch: {
scrollY(newY) {
let translateY = Math.max(this.minTranslateY, newY)
let zIndex = 0
// 图片的放大缩小
let scale = 1
// 图片的高斯模糊
let blur = 0
this.$refs.bgLayer.style[transform] = `translate3d(0, ${translateY}px, 0)`
// this.$refs.bgLayer.style['webkitTransform'] = `translate3d(0, ${translateY}px, 0)`
// 判断图片的下拉放大
const percent = Math.abs(newY / this.imageHeight) // 公式
if (newY > 0) {
scale = 1 + percent
zIndex = 10
} else {
blur = Math.min(20 * percent, 20)
}
this.$refs.filter.style[backdrop] = `blur${blur}`
// this.$refs.filter.style.webkitBackgroundFilter = `blur${blur}`
if (newY < this.minTranslateY) {
zIndex = 10
this.$refs.bgImage.style.paddingTop = 0
this.$refs.bgImage.style.height = `${LIXIT_HEIGHT}px`
} else {
this.$refs.bgImage.style.paddingTop = '70%'
this.$refs.bgImage.style.height = 0
}
// 图片的层级
this.$refs.bgImage.style.zIndex = zIndex
this.$refs.bgImage.style[transform] = `scale(${scale})`
// this.$refs.bgImage.style['webkitTransform'] = `scale(${scale}`
}
}
}
</script>
// 能力检测,利用浏览器的立减测特性
let elementStyle = document.createElement('div').style
let verdor = (() => {
let transformName = {
Webkit: 'WebkitTransform',
Moz: 'MozTransform',
O: 'OTransform',
Ms: 'MsTransform',
standard: 'transform'
}
for (let key in transformName) {
if (elementStyle[transformName[key]] !== undefined) {
return key
}
}
return false // 所有都不支持就直接返回false
})()
export function prefixStyle(style) {
if (verdor === false) {
return false
}
if (verdor === 'standard') {
return style
}
return verdor + style.charAt(0).toUpperCase() + style.substr(1)
}