vuejs2.0 高级实战 全网稀缺 音乐WebAPP

本文详述了一款基于Vue.js 2.0的音乐WebAPP的开发过程,涵盖了JSONP原理、DOM操作、Vuex状态管理、组件交互、动画效果、数据处理等多个方面。重点讲解了Vue组件的创建、Vuex的使用场景、路由间数据传递以及优化首屏加载等技术实战内容。
摘要由CSDN通过智能技术生成

src简单的介绍

这里写图片描述

入口文件main.js

import 'babel-polyfill'  //写在第一位
import Vue from 'vue'
import App from './App'
import router from './router'
import fastclick from 'fastclick'
import VueLazyload from 'vue-lazyload'
import store from './store'

import 'common/stylus/index.styl'

/* eslint-disable no-unused-vars */
// import vConsole from 'vconsole'

fastclick.attach(document.body)

Vue.use(VueLazyload, {
   
  loading: require('common/image/default.png')  //传一个默认参数
})

/* eslint-disable no-new */
new Vue({
   
  el: '#app',
  router,
  store,
  render: h => h(App)
})

babel-polyfill是es6底层铺垫即支持一些API,比如promise,balbel-runtime为es6语法转义,fastclick解决移动端点击300毫秒的延迟

devDependencies 里面的插件(比如各种loader,babel全家桶及各种webpack的插件等)只用于开发环境,不用于生产环境,因此不需要打包;而 dependencies 是需要发布到生产环境的,是要打包的。

dependencies:应用能够正常运行所依赖的包。这种 dependencies 是最常见的,用户在使用 npm install 安装你的包时会自动安装这些依赖。
devDependencies:开发应用时所依赖的工具包。通常是一些开发、测试、打包工具,例如 webpack、ESLint、Mocha。应用正常运行并不依赖于这些包,用户在使用 npm install 安装你的包时也不会安装这些依赖。

css-loader (opens new window): 处理 CSS 中的 url 与 @import,并将其视为模块引入,style-loader 用以将 CSS 注入到 DOM 中,原理为使用 DOM API 手动构建 style 标签,并将 CSS 内容注入到 style 中。

{
   
  "name": "vue-music",
  "version": "1.0.0",
  "description": "音乐播放器",
  "author": "songhao",
  "private": true,
  "scripts": {
   
    "dev": "node build/dev-server.js",
    "start": "node build/dev-server.js",
    "build": "node build/build.js",
    "lint": "eslint --ext .js,.vue src"
  },
  "dependencies": {
   
    "babel-runtime": "^6.0.0",
    "vue": "^2.3.3",
    "vue-router": "^2.5.3",
    "vuex": "^2.3.1",
    "fastclick": "^1.0.6",
    "vue-lazyload": "1.0.3",
    "axios": "^0.16.1",
    "jsonp": "0.2.1",
    "better-scroll": "^0.1.15",
    "create-keyframe-animation": "^0.1.0",
    "js-base64": "^2.1.9",
    "lyric-parser": "^1.0.1",
    "good-storage": "^1.0.1"
  },
  "devDependencies": {
   
    "autoprefixer": "^6.7.2",
    "babel-core": "^6.22.1",
    "babel-eslint": "^7.1.1",
    "babel-loader": "^6.2.10",
    "babel-plugin-transform-runtime": "^6.22.0",
    "babel-preset-env": "^1.3.2",
    "babel-preset-stage-2": "^6.22.0",
    "babel-register": "^6.22.0",
    "babel-polyfill": "^6.2.0",
    "chalk": "^1.1.3",
    "connect-history-api-fallback": "^1.3.0",
    "copy-webpack-plugin": "^4.0.1",
    "css-loader": "^0.28.0",
    "eslint": "^3.19.0",
    "eslint-friendly-formatter": "^2.0.7",
    "eslint-loader": "^1.7.1",
    "eslint-plugin-html": "^2.0.0",
    "eslint-config-standard": "^6.2.1",
    "eslint-plugin-promise": "^3.4.0",
    "eslint-plugin-standard": "^2.0.1",
    "eventsource-polyfill": "^0.9.6",
    "express": "^4.14.1",
    "extract-text-webpack-plugin": "^2.0.0",
    "file-loader": "^0.11.1",
    "friendly-errors-webpack-plugin": "^1.1.3",
    "html-webpack-plugin": "^2.28.0",
    "http-proxy-middleware": "^0.17.3",
    "webpack-bundle-analyzer": "^2.2.1",
    "semver": "^5.3.0",
    "shelljs": "^0.7.6",
    "opn": "^4.0.2",
    "optimize-css-assets-webpack-plugin": "^1.3.0",
    "ora": "^1.2.0",
    "rimraf": "^2.6.0",
    "url-loader": "^0.5.8",
    "vue-loader": "^11.3.4",
    "vue-style-loader": "^2.0.5",
    "vue-template-compiler": "^2.3.3",
    "webpack": "^2.3.3",
    "webpack-dev-middleware": "^1.10.0",
    "webpack-hot-middleware": "^2.18.0",
    "webpack-merge": "^4.1.0",
    "stylus": "^0.54.5",
    "stylus-loader": "^2.1.1",
    "vconsole": "^2.5.2"
  },
  "engines": {
   
    "node": ">= 4.0.0",
    "npm": ">= 3.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
    <title>vue-music</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

APP.vue

<template>
  <div id="app" @touchmove.prevent>
    <m-header></m-header>
    <tab></tab>
    <keep-alive>
      <router-view></router-view>
    </keep-alive>
    //全局的播放器组件
    <player></player>
  </div>
</template>

<script type="text/ecmascript-6">
  import MHeader from 'components/m-header/m-header'
  import Player from 'components/player/player'
  import Tab from 'components/tab/tab'

  export default {
   
    components: {
   
      MHeader,
      Tab,
      Player
    }
  }
</script>

jsonp原理

它发送的不是一个ajax请求,是创建了一个script标签不受同源策略的影响,通过src指向服务器的地址,在地址后面加一个callback=a,服务器解析这个URL发现有一个callback=a的参数,返回数据的时候调用这个a方法,前端定义的a方法就能直接拿到数据

最后返回的时候从1开始截取,是因为上面的方法已经添加了&符号,所有返回&符之后的内容

jsonp的封装 这个js放置于静态文件夹下

import originJsonp from 'jsonp'   //jsonp 结合promise 封装

export default function jsonp(url, data, option) {
   
  url += (url.indexOf('?') < 0 ? '?' : '&') + param(data)

  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)
    //新型写法 es6写法
    //url += `&${k}=${encodeURIComponent(value)}`   es6语法
  }
  return url ? url.substring(1) : ''
}


推荐页面 recommend.js 使用jsonp 调取轮播图的数据

用到了es6对象的合并方法Object.assign,浅拷贝的方法

import jsonp from 'common/js/jsonp'
import {
   commonParams, options} from './config'

export function getRecommend() {
   
  const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'

  const data = Object.assign({
   }, commonParams, {
     //assign es6语法
    platform: 'h5',
    uin: 0,
    needNewCode: 1
  })

  return jsonp(url, data, options)
}

config.js 因为数据是爬的,所以定义了通用的参数对象,私有的参数通过assign方法添加

export const commonParams = {
   
  g_tk: 1928093487,
  inCharset: 'utf-8',
  outCharset: 'utf-8',
  notice: 0,
  format: 'jsonp'
}

export const options = {
   
  param: 'jsonpCallback'
}

export const ERR_OK = 0

components/recommend.vue 在组件中调用接口

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
    }
  }
<div v-if="recommends.length" class="slider-wrapper" ref="sliderWrapper">
          <slider>
            <div v-for="item in recommends">
              <a :href="item.linkUrl">
                <img class="needsclick" @load="loadImage" :src="item.picUrl">
                <!-- 如果fastclick监听到有class为needsclick就不会拦截 -->
              </a>
            </div>
          </slider>
        </div>

这里用到了slider组件以及slot的知识,也遇到了一个坑,因为数据响应 必须确定有数据v-if="recommends.length"才能保证插槽的正确显示

dom.js ,比较重要

操作dom的文件,位于通用静态文件

//是否有指定class存在
export function hasClass(el, className) {
   
  let reg = new RegExp('(^|\\s)' + className + '(\\s|$)')
  return reg.test(el.className)
}
//如果存在什么都不做,否则就设置添加
export function addClass(el, className) {
   
  if (hasClass(el, className)) {
   
    return
  }

  let newClass = el.className.split(' ')
  newClass.push(className)
  el.className = newClass.join(' ')
}

//展现了方法的设置技巧,一个getter、一个setter
export function getData(el, name, val) {
   
  const prefix = 'data-'
  if (val) {
   
    return el.setAttribute(prefix + name, val)
  }
  return el.getAttribute(prefix + name)
}
//下面的方法,在歌手详情页的自组件music-list用到,用于控制属性的兼容性
let elementStyle = document.createElement('div').style

let vendor = (() => {
   
  let transformNames = {
   
    webkit: 'webkitTransform',
    Moz: 'MozTransform',
    O: 'OTransform',
    ms: 'msTransform',
    standard: 'transform'
  }

  for (let key in transformNames) {
   
    if (elementStyle[transformNames[key]] !== undefined) {
   
      return key
    }
  }

  return false
})()

export function prefixStyle(style) {
   
  if (vendor === false) {
   
    return false
  }

  if (vendor === 'standard') {
   
    return style
  }
//数据的拼接,首字母大写加上剩余的部分
  return vendor + style.charAt(0).toUpperCase() + style.substr(1)
}

从下面开始开始编写项目

首先是推荐页面,由轮播图和热门歌单组成

轮播图的数据通过jsonp可以得到,但是热门歌单因为有referer、host的认证,所以需要在dev-server.js设置代理,(欺骗服务器)用到axios而不是jsonp

bulid目录下dev-server.js处理代理

require('./check-versions')()

var config = require('../config')
if (!process.env.NODE_ENV) {
   
  process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}

var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')
var axios = require('axios') //第一步

// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable

var app = express()

var apiRoutes = express.Router()   //以下是后端代理接口 第二步

apiRoutes.get('/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)  //输出到浏览器的res
  }).catch((e) => {
   
    console.log(e)
  })
})

apiRoutes.get('/lyric', function (req, res) {
     //这是另一个接口下节将用到
  var url = 'https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg'

  axios.get(url, {
   
    headers: {
   
      referer: 'https://c.y.qq.com/',
      host: 'c.y.qq.com'
    },
    params: req.query
  }).then((response) => {
   
    var ret = response.data
    if (typeof ret === 'string') {
   
      var reg = /^\w+\(({
   [^()]+})\)$/
      var matches = ret.match(reg)
      if (matches) {
   
        ret = JSON.parse(matches[1])
      }
    }
    res.json(ret)
  }).catch((e) => {
   
    console.log(e)
  })
})

app.use('/api', apiRoutes)   //最后一步

var compiler = webpack(webpackConfig)

var devMiddleware = require('webpack-dev-middleware')(compiler, {
   
  publicPath: webpackConfig.output.publicPath,
  quiet: true
})

var hotMiddleware = require('webpack-hot-middleware')(compiler, {
   
  log: () => {
   }
})
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
   
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
   
    hotMiddleware.publish({
    action: 'reload' })
    cb()
  })
})

// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
   
  var options = proxyTable[context]
  if (typeof options === 'string') {
   
    options = {
    target: options }
  }
  app.use(proxyMiddleware(options.filter || context, options))
})

// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())

// serve webpack bundle output
app.use(devMiddleware)

// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)

// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))

var uri = 'http://localhost:' + port

var _resolve
var readyPromise = new Promise(resolve => {
   
  _resolve = resolve
})

console.log('> Starting dev server...')
devMiddleware.waitUntilValid(() => {
   
  console.log('> Listening at ' + uri + '\n')
  // when env is testing, don't need open it
  if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
   
    opn(uri)
  }
  _resolve()
})

var server = app.listen(port)

module.exports = {
   
  ready: readyPromise,
  close: () => {
   
    server.close()
  }
}

recommend.js 使用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)
  })
}

slider轮播图组件用到了dom.js中的addclass方法,引入了better-scroll,注意一下window.addEventListener,初始化的时候这个方法this._setSliderWidth(true)传入true,来控制2倍的dom复制,特别注意初始化dots的方法

<template>
  <div class="slider" ref="slider">
    <div class="slider-group" ref="sliderGroup">
      <slot></slot>
    </div>
    <div class="dots">
      <span
        class="dot"
        :class="{active: currentPageIndex === index }"
        v-for="(item, index) in dots"
      ></span>
    </div>
  </div>
</template>

<script type="text/ecmascript-6">
import {
    addClass } from "common/js/dom";
import BScroll from "better-scroll";

export default {
   
  name: "slider",
  props: {
   
    loop: {
   
      type: Boolean,
      default: true
    },
    autoPlay: {
   
      type: Boolean,
      default: true
    },
    interval: {
   
      type: Number,
      default: 4000
    }
  },
  data() {
   
    return {
   
      dots: [],
      currentPageIndex: 0
    };
  },
  mounted() {
   
    setTimeout(() => {
   
      this._setSliderWidth();
      this._initDots();
      this._initSlider();

      if (this.autoPlay) {
   
        this._play();
      }
    }, 20);

    window.addEventListener("resize", () => {
   
      if (!this.slider) {
   
        return;
      }
      this._setSliderWidth(true);
      this.slider.refresh();
    });
  },
  //keep-active下的声明周期,相当于小程序的onshow
  activated() {
   
    if (this.autoPlay) {
   
      this._play();
    }
  },
  //组件销毁后清除定时器,有利于内存释放
  deactivated() {
   
    clearTimeout(this.timer);
  },
  beforeDestroy() {
   
    clearTimeout(this.timer);
  },
  methods: {
   
    //计算轮播的宽度
    _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;
      }
      if (this.loop && !isResize) {
   
        width += 2 * sliderWidth;
      }
      this.$refs.sliderGroup.style.width = width + "px";
    },
    //初始化BScroll
    _initSlider() {
   
      this.slider = new BScroll(this.$refs.slider, {
   
        scrollX: true,
        scrollY: false,
        momentum: false,
        snap: true,
        snapLoop: this.loop,
        snapThreshold: 0.3,
        snapSpeed: 400
      });

      // 设置currentPageIndex
      this.slider.on("scrollEnd", () => {
   
        let pageIndex = this.slider.getCurrentPage().pageX;
        if (this.loop) {
   
          pageIndex -= 1;
        }
        this.currentPageIndex = pageIndex;

        if (this.autoPlay) {
   
          this._play();
        }
      });
      //手动出发的时候清楚定时器
      this.slider.on("beforeScrollStart", () => {
   
        if (this.autoPlay) {
   
          clearTimeout(this.timer);
        }
      });
    },
    //初始化dots
    _initDots() {
   
      this.dots = new Array(this.children.length);
    },
    //轮播关键实现
    _play() {
   
      let pageIndex = this.currentPageIndex + 1;
      if (this.loop) {
   
        pageIndex += 1;
      }
      this.timer = setTimeout(() => {
   
        this.slider.goToPage(pageIndex, 0, 400);
      }, this.interval);
    }
  }
};
</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;
        width: 100%;
        overflow: hidden;
        text-decoration: none;
      }

      img {
   
        display: block;
        width: 100%;
      }
    }
  }

  .dots {
   
    position: absolute;
    right: 0;
    left: 0;
    bottom: 12px;
    text-align: center;
    font-size: 0;

    .dot {
   
      display: inline-block;
      margin: 0 4px;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: $color-text-l;

      &.active {
   
        width: 20px;
        border-radius: 5px;
        background: $color-text-ll
  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值