【音樂App】—— Vue-music 專案學習筆記:推薦頁面開發
阿新 • • 發佈:2018-12-22
前言:以下內容均為學習慕課網高階實戰課程的實踐爬坑筆記。
上一篇總結了專案概述、專案準備、頁面骨架搭建。這一篇重點梳理推薦頁面開發。專案github地址:https://github.com/66Web/ljq_vue_music,歡迎Star。
一、頁面簡介+輪播圖資料分析 |
- 資料:從QQ音樂抓取的真實資料
輪播圖 | 熱門歌單推薦 |
二、JSONP原理介紹 |
- 一句話解釋JSONP原理:動態生成一個JavaScript標籤,其src由介面url、請求引數、callback函式名拼接而成;利用js標籤沒有跨域限制的特性實現跨域請求
- 有幾點需要注意:
- callback函式要繫結在window物件上
- 服務端返回資料有特定格式要求:callback函式名+’(‘+JSON.stringify(返回資料) +’)’
- 不支援post,因為js標籤本身就是一個get請求
- 什麼是Promise:
- 簡單說就是一個容器,裡面儲存著某個未來才會結束的事件 (通常是一個非同步操作)的結果。
- 從語法上說,Promise是一個物件,從它可以獲取非同步操作的訊息
- Promise基本用法:
- ES6規定,Promise物件是一個建構函式,用來生成Promise例項
var
- Promise建構函式接受一個函式作為引數,該函式的兩個引數分別是resolve和reject。它們是兩個函式,由JavaScript引擎提供,不是自己部署。
- Promise例項生成以後,可以用then方法分別制定Resolved狀態和Rejected狀態的回撥函式:
promise.then(function
三、JSONP |
- github地址:https://github.com/webmodules/jsonp
- 安裝JSONP依賴:
npm install jsonp --save
四、封裝JSONP、Primise |
- common->js目錄下: 建立 jsonp.js
import originJSONP from 'jsonp' export default function jsonp(url, data, option) { url += (url.indecOf('?') < 0 ? '?' : '&') + param(data); return new Promise((resolve, reject) => { originJSONP(url, option, (err, data) => { if(!err){ resolve(data) }else{ reject(err) } }) }) } 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) : '' }
五、JSONP的應用+輪播圖資料抓取 |
- api目錄下建立 config.js:配置與介面統一的引數
/** * 為了和QQ音樂介面一致,配置一些公用的引數、options和err_num碼 */ 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
- api目錄下建立 recommend.js:
import jsonp from '@/common/js/jsonp' import {commonParames, options} from './config' export function getRecommend() { const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg' const data = Object.assign({}, commonParames, { platfrom: 'h5', uin: 0, needNewCode: 1 }) return jsonp(url, data, options) }
- recommend.vue中呼叫並獲取資料
import {getRecommend} from '@/api/recommend' import {ERR_OK} from '@/api/config' export default { created() { this._getRecommend(); }, methods: { _getRecommend() { getRecommend().then((res) => { if(res.code === ERR_OK) { console.log(res.data.slider) } }) } } }
六、 輪播圖元件實現 |
- base目錄下: 建立slider.vue元件
- 插槽<slot></slot>:外部引用slider.vue時,<slider></slider>裡面包裹的DOM,會被插入到插槽的部分
<div class="slider-group"> <slot></slot> </div>
- recommend.vue 中編寫插槽中的DOM:
<slider> <div v-for="(item, index) in recommends" :key="index"> <a :href="item.linkUrl"> <img :src="item.picUrl"> </a> </div> </slider>
- slider.vue 中指定需要從父元件接收的屬性:loop是否迴圈、autoPlay是否自動播放、interval間隔時間
props: { loop: { type: Boolean, default: true }, autoPlay: { type: Boolean, default: true }, interval: { type: Number, default: 4000 } }
- 橫向滾動:使用better-scroll
better-scroll中文文件:https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/options-advanced.html#snap better-scroll中的相關選項:
|
- 安裝better-scroll依賴:
npm install better-scroll --save
- slider.vue 中引用:
import BScroll from 'better-scroll'
-
ref引用外層容器和內層元素:
<div class="slider" ref="slider"> <div class="slider-group" ref="sliderGroup">
-
common->js目錄下建立 dom.js:封裝一些DOM操作相關的程式碼
//為元素新增Class、判斷元素是否有指定class export function addClass(el, className){ if(hasClass(el, className)){ return } let newClass = el.className.split(' ') newClass.push(className) el.className = newClass.join(' ') } export function hasClass(el, className){ let reg = new RegExp('(^|\\s)' + className + '(\\s|$)') return reg.test(el.className) }
- slider.vue 中引用:
import {addClass, hasClass} from '@/common/js/dom'
-
在methods中定義兩個方法:設定slider寬度、初始化slider
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')//為迴圈生成的slider子元素,動態新增slider-item class child.style.width = sliderWidth + 'px'//不要忘記加單位! width += sliderWidth } if(this.loop){ //如果loop為true,BScroll的snap屬性會左右克隆兩個DOM,保證迴圈切換 width += 2 * sliderWidth } this.$refs.sliderGroup.style.width = width + 'px'//不要忘記加單位! }, _initSilder() { this.slider = new BScroll(this.$refs.slider,{ scrollX: true, //橫向滾動 scrollY: false, //禁止縱向滾動 momentum: false,//禁止慣性運動 snap: { loop: this.loop, threshold: 0.3, speed: 400 } }) } }
- 初始化BScroll的時機:必須保證元件已經渲染好了,DOM高度已經被撐開
//在mouted生命鉤子中通過setTimeout呼叫: mouted() { setTimeout(() => { this._setSliderWidth() this._initSlider() }, 20) }
- 坑:recommend.vue中直接引用了<slider>,recommends的引用時機是在created()中呼叫了_getRecommend(),_getRecommend()的這個時間是一個非同步過程,可能會有延遲,因為它取的是真實資料;因此,當recommends還沒有get到時,即還沒有填入任何資料時,slider.vue中的mouted()實際上已經執行了。
- 解決:recommend.vue中為slider-wrapper新增v-if="recommends.length",確保recommends陣列中有內容時,才渲染<slider>
<div v-if="recommends.length" class="slide-wrapper">
- 新增dots區塊,實現自動輪播
- data中維護一個數據dots,預設是一個空陣列
dots: []
- methods中初始化Dots:
_initDots() { this.dots = new Array(this.children.length) }
- 渲染dots:
<span class="dot" v-for="(item, index) in dots" :key="index"></span>
- 選中高亮:
/** data中維護一個數據currentPageIndex:0,表示當前預設是第一頁 * v-bind動態繫結 :class="{active: currentPageIndex === index}"> * 在_initSlider()方法中給slider新增事件: */ this.slider.on('scrollEnd', () => { //當一個頁面滾動完畢後,會派發一個scrollEnd事件 let pageIndex = this.slider.getCurrentPage().pageX //獲得slider的pageIndex if(this.loop) { //如果是迴圈,snap會預設給子元素前面增加一個拷貝 pageIndex -= 1 //要得到實際的pageIndex,pageInde需要-1 } this.currentPageIndex = pageIndex })
- 自動播放:
//mounted()->setTimeout中判斷autoplay屬性,呼叫_play(): if(this.autoplay) { this._play() } //methods中定義_play(): _play() { let pageIndex = this.currentPageIndex + 1;//this.currentPageIndex從0開始的 if(this.loop) { pageIndex += 1//loop為true時,最開始有一個複製的副本,實際的pageIndex需要+1 } this.timer = setTimeout(() => { //頁面的切換,利用BScroll的介面goToPage this.slider.goToPage(pageIndex, 0, 400) //引數:X方向、Y方向、時間間隔 },this.interval) }
- 坑:使用setTimeout,只會執行一次,從第一張自動滾動到第二張就停止了。
- 解決:scrollEnd事件中新增:
if(this.autoPlay) { this._play() }
- 坑:自動滾動後不到400ms時,手動滑動後又執行了自動滾動,體驗效果會很奇怪
- 解決:slider 新增 beforeScrollStart事件
this.slider.on('beforeScrollStart', () => { if (this.autoPlay) { clearTimeout(this.timer) } })
- 坑:在滾動中,改變視口大小,圖片會同時顯示兩張,因為之前設定好的width都沒變
- 解決:mounted中監聽window的resize事件 —— 視窗改變事件,當視窗改變時,重新呼叫_setSlideWidth()
- 坑:如果視窗變和不變時都呼叫_setSlideWidth(),就會執行兩次width += 2 * sliderWidth,這一定是不對的
- 解決:呼叫_setSlideWidth(),需要同時傳入一個引數,用來判斷視窗是否改變了
window,addEventListener('resize',(() => { if(!this.slider) { return } this._setSliderWidth(true) this.slider.refresh() })) _setSliderWidth(isResize) { //其它程式碼 if(this.loop && !isResize){ width += 2 * sliderWidth }
} -
App.vue 中優化:快取DOM到記憶體中,不用重新發送請求,這樣slider就不會有閃動的現象
<keep-alive> <router-view></router-view> </keep-alive>
- slider中優化:當元件中有定時器,一定要記得在元件銷燬時清理掉這些定時器,使用生命週期destroyed()
destroyed() { clearTimeout(this.timer) }
七、歌單資料介面分析 |
問題: QQ音樂歌單資料的請求頭中有域名Host、來源Referer,所以請求的介面應該是有加上該域名和來源,直接請求就會報HTTP-500錯誤。
原因: 前端不能直接修改request header,所以要通過後端代理的方式解決。
解決: 採用 axios 在node.js中傳送http請求
- 安裝axios:
npm install axios --save
- build->webpack.dev.conf.js
- 定義路由,通過axios傳送一個Http請求,同時修改header中的和QQ相關的Host、Referer,
- 將瀏覽器傳遞過來的引數全部傳給服務端,然後通json響應的內容輸出到瀏覽器端。
- 在 const portfinder = require('portfinder') 後新增:
const express = require('express') const axios = require('axios') const app = express() var apiRoutes = express.Router() app.use('/api', apiRoutes)
- devServer 中新增:
before(app) { //定義getDiscList介面,回撥傳入兩個引數,前端請求這個介面 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: { //通過node請求QQ介面,傳送http請求時,修改referer和host referer: 'https://y.qq.com/', host: 'c.y.qq.com' }, params: req.query //把前端傳過來的params,全部給QQ的url }).then((response) => { //成功與失敗的回撥 res.json(response.data) }).catch((e) => { console.log(e) }) })
- recommend.js中:
import axios from 'axios'; export function getDiscList() { // const url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg' 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' //使用的時axios,所以format使用的是json,不是jsonp }) // return jsonp(url, data, options) return axios.get(url, { params: data }).then((res) => { return Promise.resolve(res.data) //es6新語法,返回一個以給定值解析後的Promise物件 }) }
|
- recommend.vue中:定義和呼叫獲取資料的方法
//created()中: this._getDiscList(); //methods中: _getDiscList() { getDiscList().then((res) => { if(res.code === ERR_OK) { console.log(res.data) } }) }
八、歌單列表元件開發和資料的應用 |
- data中定義資料:
discList: []
- _getDiscList()中將返回的資料list賦給discList:
this.discList = res.data.list
- 使用 v-html="item.creator.name" 給html字元做轉義
<div class="recommend-list"> <h1 class="list-title">熱門歌單推薦</h1> <ul> <li v-for="(item, index) in discList" :key="index" class="item"> <div class="icon"> <img :src="item.imgurl" width="60" height="60"> </div> <div class="text"> <h2 class="name" v-html="item.creator.name"></h2> <p class="desc" v-html="item.dissname"></p> </div> </li> </ul> </div>
- CSS樣式:經典flex佈局
- 左邊固定寬高,右邊根據手機視口寬度自適應
- 右側:
.item display: flex align-items:center //水平方向居中
- 右側文字內容:
.text display: flex flex-direction: column //縱向排列 justify-content: center //垂直居中
- 一個元素,既可以是flex佈局的item,同時也可做flex佈局
九、scroll元件的抽象和應用 |
- better-scroll滾動佈局:只會滾動父元素下的第一個子元素 —— 想要slider和recommend-list同時可以滾動,需要在外層再巢狀一個<div>,將兩個元素包裹起來
- 抽象出scorll元件 -- 基礎元件
- base->scroll目錄下: 建立 scroll.vue
- 佈局DOM:一個wrapper加一個插槽
<template> <div ref="wrapper"> <slot></slot> </div> </template>
-
引入BScroll:
import BScroll from 'better-scroll'
- 需要傳入props引數:
props: { //probeType: 1 滾動的時候會派發scroll事件,會截流。2 滾動的時候實時派發scroll事件,不會截流 。3 除了實時派發scroll事件,在swipe的情況下仍然能實時派發scroll事件 probeType: { type: Number, default: 1 }, // click: true 是否派發click事件,通常判斷瀏覽器派發的click還是betterscroll派發的click,可以用event._constructed,若是bs派發的則為true click: { type: Boolean, default: true }, data: { type: Array, default: null } }
- 確保DOM已經渲染,再執行_initScroll:
mouted() { setTimeout(() => { //確保DOM已經渲染 this. _initScroll() }, 20) }
- methods中定義初始化scroll的方法,並代理幾個必需的方法:
methods: { _initScroll() { if(!this.$refs.wrapper){ return } this.scroll = new BScroll(this.$refs.wrapper, { probeType : this.probeType, click: this.click }) }, enable() { // 啟用 better-scroll,預設開啟 this.scroll && this.scroll.enable() }, disable() { // 禁用better-scroll, 如果不加,scroll的高度會高於內容的高度 this.scroll && this.scroll.disable() }, refresh() { // 強制 scroll 重新計算,當 better-scroll 中的元素髮生變化的時候呼叫此方法 this.scroll && this.scroll.refresh() } }
- watch監聽data資料:
watch: { data() { //監測data的變化 setTimeout(() => { this.refresh() }, 20) } }
- 後面在專案的開發中,可以根據需要再隨時新增props引數和methods代理方法
- recommend.vue 中使用:
- 引用scroll元件:
import Scroll from '@/base/scroll/scroll'
- 把class="recommend-content"的<div>改成<scroll>
- 坑:此時scroll已經初始化了,但還不能滾動
- 原因:scroll初始化的時機,是在scroll元件的mounted();但<scroll>包含的DOM是由獲取到的data資料填充撐開高度才可以滾動,此時還沒撐開,就滾動不了;當資料改變後,scroll應該改變
- 解決:<scroll>傳入一個數據 :data="discList";當資料discList接收到時,scroll元件中的watch監聽到這個變化,就會強制scroll重新計算
- 坑:因為整個頁面會有兩個部分都是請求資料,當_getRecommend()的請求時間大於this._getDiscList()的時候,頁面的高度就不夠
- 如果:如下 ↓ 滾動的高度就會差一個slider的高度,滾不到底部。
因為refresh()之前,slider的資料還沒有渲染出來,scroll會認為,需要滾動的高度,只是列表的高度created() { setTimeout(() => { this._getRecommend(); }, 1000) this._getDiscList(); }
- 實際中,並不能知道兩個部分,哪一個會先出現,需要注意還有一個坑:不能用計算屬性計算兩個部分的資料
- 原因:與圖片的載入,視口的大小(實時圖片的寬高)有關。
- 解決:給<img>新增onload事件
<img :src="item.picUrl" @load="loadImage">
loadImage() { if(!this.checkloaded){ //新增一個標誌位,如果load一次了,就不再執行onload事件了 this.checkloaded = true this.$refs.scroll.refresh() } }
十、 lazyload懶載入外掛介紹和應用 |
- 歌單優化:歌單是由很多張圖片組成的,使用vue-lazyload外掛 解決圖片懶載入 的問題
- vue-lazyload github地址: https://github.com/hilongjw/vue-lazyload
- 安裝外掛:
npm install vue-lazyload --save
- 引用註冊: main.js 中
import VueLazyload from 'vue-lazyload' Vue.use(VueLazyload, { loading: require('@/common/image/default.png') //loading時預設顯示的圖片 })
- 使用外掛:recommend.vue 中把歌單列表<img>中原來的 :src替換為v-lazy
<img v-lazy="item.imgurl" width="60" height="60">
- 這樣,只有使用者滾動過的地方,圖片才會載入,沒有看的地方,就不會進行載入
- 問題:fastclick和better-scroll的click會有衝突.
- 解決:slider中的<img>新增一個class="needsclick",這是fastclick中的一個屬性
<img class="needsclick" :src="item.picUrl" @load="loadImage">
十一、 loading基礎元件的開發和應用 |
- 優化體驗:在歌單列表沒有渲染好之前,展示一個轉圈loading
- 佈局DOM:
<div class="loading"> <img width="24" height="24" src="./loading.gif"> <p class="desc">{{title}}</p> </div>
- props引數:
props: { title: { type: String, default: '正在載入...' } }
- CSS樣式: View Code
- recommend.vue 中引用註冊,在<scroll>中使用:
<div class="loading-container" v-show="!disList.length"> <loading></loading> </div>
注:專案來自慕課網