初始化项目
初始化HTML中的代码
利用rem+视口释放的方式来适配移动端
注意点: 如果在HTML文件中用到了字符串模板, 字符串模板中用到了变量, 那么html-plugin是无法处理的,所以会报错,如果想解决这个问题,那么我们需要再借助一个loader, html-loader
借助postcss-pxtorem实现自动将px转换成rem
借助webpack实现CSS3/ES678语法的兼容
借助fastclick解决移动端100~300ms的点击事件延迟问题
初始化默认的全局样式
注意点: 在移动端开发中, 一般情况下我们不需要让字体大小随着屏幕尺寸的变化而变化
由于我们是通过视口缩放来适配移动端的, 所以我们不能直接设置字体大小, 否则字体大小就会随着屏幕尺寸的变化而变化
- 注意初始化的文件需要存放导致的位置,和相关的安装和配置(html-loader,postcss-pxtorem,fastclick)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!--可以让部分国产浏览器默认采用高速模式渲染页面-->
<meta name="renderer" content="webkit">
<!--为了让 IE 浏览器运行最新的渲染模式下-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--SEO三大标签-->
<title>WY音乐</title>
<meta name="keywords"
content="网易云音乐,音乐,播放器,网易,下载,播放,DJ,免费,明星,精选,歌单,识别音乐,收藏,分享音乐,音乐互动,高音质,320K,音乐社交,官网,移动站,music.163.com">
<meta name="description" content="网易云音乐是一款专注于发现与分享的音乐产品,依托专业音乐人、DJ、好友推荐及社交功能,为用户打造全新的音乐生活。">
<!--
apple-touch-icon: 是苹果私有的属性
作用: 指定将网页保存到主屏幕上的时候的图标
-->
<link rel="apple-touch-icon" href="./apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="114x114" href="./apple-touch-icon114.png">
<link rel="apple-touch-icon" sizes="152x152" href="./apple-touch-icon152.png">
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon180.png">
<!--网页快捷图标-->
<link rel="icon" href="./favicon.ico">
<script>
let scale = 1.0 / window.devicePixelRatio;
let text = `<meta name="viewport" content="width=device-width, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}, user-scalable=no">`;
document.write(text);
document.documentElement.style.fontSize = window.innerWidth / 7.5 + "px";
document.documentElement.setAttribute('data-dpr', Math.round(window.devicePixelRatio) + '');
document.documentElement.setAttribute('data-theme', 'theme');
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import fastclick from 'fastclick'
import './assets/css/base.scss'
// 解决移动端100~300ms延迟的问题
fastclick.attach(document.body)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
首页制作
效果图
1. 整体换肤效果的实现(localStorage)
- 定义scss的mixin函数,根据头部的data-theme主题,设置背景颜色、文字颜色
- 主题存入localStorage
@mixin bg_color(){
background: $background-color-theme;
// 属性选择器
[data-theme=theme1] & {
background: $background-color-theme1;
}
[data-theme=theme2] & {
background: $background-color-theme2;
}
}
// 存入localStorage
localStorage.setItem("theme", this.themes[this.index]);
mounted() {
// 获取设置主题
document.documentElement.setAttribute(
"data-theme",
localStorage.getItem("theme")
);
}
2. 图标适应屏幕
- 根据html标签的data-dpr属性来设置大图/中图/小图/字体适应大小不变
/*根据dpr计算font-size*/
@mixin font_dpr($font-size){
font-size: $font-size;
[data-dpr="2"] & { font-size: $font-size * 2;}
[data-dpr="3"] & { font-size: $font-size * 3;}
}
3.顶栏的封装
4.导航栏的路由切换(vue-router)+ 路由懒处理
<template>
<div class="tabbar">
<router-link tag="div" class="item" to="/recommend">
<span>推荐</span>
</router-link>
<router-link tag="div" class="item" to="/singer">
<span>歌手</span>
</router-link>
<router-link tag="div" class="item" to="/rank">
<span>排行</span>
</router-link>
<router-link tag="div" class="item" to="/search">
<span>搜索</span>
</router-link>
</div>
</template>
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 懒加载处理
const Recommend = () => import("../views/recommend/Recommend")
const Singer = () => import("../views/singer/Singer")
const Rank = () => import("../views/rank/Rank")
const Search = () => import("../views/search/Search")
const routes = [
{
path: '',
// 默认路由
redirect: '/recommend'
},
{
path: '/recommend',
component: Recommend
},
{
path: '/singer',
component: Singer
},
{
path: '/rank',
component: Rank
}
,{
path: '/search',
component: Search
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
5.网络请求的封装(axios)
- 封装get / post 请求
- 管理请求请求地址
import axios from 'axios'
// 进行一些全局配置
axios.defaults.baseURL = 'http://localhost:3000/'
axios.defaults.timeout = 5000
// 封装自己的get/post方法
export default {
get: (path = "", data = {}) => {
return new Promise((resolve, reject) => {
axios.get(path, {
params: data
}).then(response => {
resolve(response.data);
}).catch(error => {
reject(error);
})
})
},
post: (path = "", data = {}) => {
return new Promise((resolve, reject) => {
axios.get(path, data).then(response => {
resolve(response.data);
}).catch(error => {
reject(error);
})
})
}
}
// 专门用于管理请求请求地址
import Network from './network'
// 轮播图
export const getBanner = () => Network.get('banner?type=2')
6.轮播图的展示(vue-awesome-swiper)
此处踩坑:swiper的bug, 如果数据时网络获取的,轮播图到最后一张后停止轮播
解决:swiper标签加上v-if="数据.length > 0"条件解决
再次踩坑:报组件未定义错误
解决:导入Swiper,SwiperSlide 首字母必须大写,注册组件也需要大写
再再大意:必须安装swiper,vue-awesome-swiper 两个库
<template>
<!-- swiper的bug, 如果数据时网络获取的,轮播图到最后一张后停止轮播 -->
<!-- 加上v-if="数据.length > 0"条件解决 -->
<swiper :options="swiperOption" class="banner" v-if="banners.length > 0">
<!-- slides -->
<swiperSlide v-for="value in banners" :key="value.bannerId">
<img :src="value.pic" />
</swiperSlide>
<!-- Optional controls -->
<div class="swiper-pagination" slot="pagination"></div>
</swiper>
</template>
<script>
// 这里有坑: Swiper,SwiperSlide 首字母必须大写,注册组件也需要大写
// 否则报组件未定义错误
import { Swiper, SwiperSlide } from "vue-awesome-swiper";
import "swiper/css/swiper.css"; // 必须安装swiper,vue-awesome-swiper
export default {
name: "Banner",
props: {
banners: {
type: Array,
required: true
}
},
data() {
return {
swiperOption: {
loop: true, //循环模式
autoplay: {
delay: 1500, //自动切换时间 ms
stopOnLastSlide: false, //当切换到最后一个slide是停止切换
disableOnInteraction: false // 用户操作swiper之后,是否禁止autoplay
},
// 分页器
pagination: {
el: ".swiper-pagination"
},
// 异步加载
observer: true,
observeParents: true,
observeSlideChildren: true
}
};
},
components: {
Swiper,
SwiperSlide
}
};
</script>
<style lang="scss" scoped>
.banner {
img {
width: 100%;
height: 300px;
}
}
</style>
7.个性化展示(组件复用,过滤器使用)
- 两个组件样式一样,可以封装成一个组件(personalized),传入不同的数据渲染
<template>
<div class="personalized">
<div class="personalized-top">
<h3>{{title}}</h3>
</div>
<div class="personalized-list">
<div class="item" v-for="value in personalized" :key="value.id">
<div class="image">
<img :src="value.picUrl" :alt="value.name" />
<div>
<i></i>
<span>{{value.playCount | playCount}}</span>
</div>
</div>
<p>{{value.name}}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Personalized",
props: {
personalized: {
type: Array,
required: true
},
title: {
type: String,
required: true
}
},
filters: {
// 歌单播放量
playCount(value) {
if (value >= 10000) {
return parseInt(value / 10000) + "万";
if (value > 100000000) {
return parseInt(value / 100000000) + "亿";
}
} else {
return value;
}
}
}
};
</script>
8.最新音乐展示
- 数据处理(filters)
<template>
<div class="song">
<div class="song-top">
<h3>最新音乐</h3>
</div>
<ul class="song-list">
<li v-for="value in songs" :key="value.id" class="item">
<img :src="value.song.album.picUrl" />
<div>
<h3>{{value | getSongName}}</h3>
<p>{{value.song.artists | getSingers}}</p>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "SongList",
props: {
songs: {
type: Array,
required: true
}
},
filters: {
getSingers(value) {
let singers = value[0].name;
let len = value.length;
while (len > 1) {
singers += "、" + value[len - 1].name;
len -= 1;
}
return singers;
},
getSongName(value) {
let name = "";
value.song.alias.length > 0 ? (name = value.name + value.song.alias[0]) : (name = value.name);
return name
}
}
};
</script>
9.图片懒加载处理(vue-lazyload)
- 解释:进入主页,系统默认会加载所有的图片,而用户只能看到一屏的图片,造成加载缓慢,用户体验不好
- 安装:npm install vue-lazyload --save
- 在main.js注册、使用
// 导入 vue-lazyload
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
// 设置占位图片
loading: require('./assets/images/placeholder.png')
})
- 将需要懒加载图片的 :src 修改为 v-lazy
10.滚动效果(IScroll)
利用IScroll插件来滚动
-
遇坑:拖拽没效果
问题:区域高度和内容高度等高,没有滑动效果
解决:设置区域内容与需要滚动的视口区域等高
-
再遇坑:拖拽卡顿
问题:元素原生的滚动事件有冲突
解决:html,body标签选择器 加上 overflow: hidden; touch-action: none;
11.拖拽组件的封装(IScroll、slot 、MutationObserver)
利用具名插槽来封装滚动组件
-
踩坑:不能滚动
原因:由于数据是网络加载过来的,在IScroll计算高度是,高度不正确
处理1:利用计时器延时几秒,再计算高度。缺点:加载时间不能确定,用户需要等待,体验差,不推荐
处理2:利用MutationObserver构造函数,监听元素的改变,重新计算高度
-
踩坑:滚动卡顿
解决:禁用其他事件
scrollX: false,
scrollY: true,
disablePointer: true,
disableTouch: false,
disableMouse: true
<template>
<div id="wrapper" ref="wrapper">
<div>
<slot></slot>
</div>
</div>
</template>
<script>
// 导入iscroll
import IScroll from "iscroll/build/iscroll-probe";
export default {
name: "ScrollView",
mounted() {
// 这里能拿到页面元素
this.iscroll = new IScroll(this.$refs.wrapper, {
mouseWheel: true,
scrollbars: true,
// 解决拖拽卡顿问题(禁用其他事件)
scrollX: false,
scrollY: true,
disablePointer: true,
disableTouch: false,
disableMouse: true
});
// 由于数据时网络获取,这里需要重新设置高度(不推荐使用计时器)
// setTimeout(() => {
// this.iscroll.refresh();
// }, 3000);
// 1.创建一个观察者对象
/**
* MutationObserver 构造函数只要监听到了指定内容发生类变化,就会执行传入的回调函数
* mutationList: 发生变化的数组
* observer: 观察者对象
*/
let observer = new MutationObserver((mutationList, observer) => {
this.iscroll.refresh();
});
// 2.告诉观察者对象,需要观察什么内容
let config = {
childList: true, // 观察目标子节点的变化,添加或删除
subtree: true, // 默认为false,设置为true可以观察后代子节点
attributeFilter: ["height", "offsetHeight"] // 观察特定属性
};
// 3.告诉观察者对象,我们需要观察什么谁,观察什么内容
/**
* 第一个参数: 观察谁
* 第二个参数: 观察什么内容
*/
observer.observe(this.$refs.wrapper, config);
}
};
</script>