Taohong的博客


  • Home

  • Archives

linux服务器配置

Posted on 2017-08-16 | In 技术

linux服务器配置

更新:yum

1
yum -y update

下载软件

1
yum -y install wget

解压软件

1
yum -y install tar

编辑器

1
yum -y install vim

安装nginx

1
yum -y install nginx

nodejs版本管理

1
2
3
4
5
6
git clone https://github.com/creationix/nvm.git ~/.nvm && cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`
. ~/.nvm/nvm.sh
# 全局配置
vi ~/.bashrc
# 加一句
source ~/.nvm/nvm.sh

安装 Nodejs

nvm install 4.4.3

淘宝镜像

1
npm install -g cnpm --registry=https://registry.npm.taobao.org

安装pm2

1
cnpm install pm2 -g

写入配置文件

1
2
3
4
# 进入配置文件目录
cd /etc/nginx/conf.d/
# 新建配置
vi blog.conf

服务配置

1
2
3
4
5
6
7
8
server {
listen 80;
server_name blog.taohong.space; #域名

location / {
proxy_pass http://127.0.0.1:3000; # 端口
}
}

静态文件配置

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name resume.taohong.space; #域名

location / {
root /project/resume/docs; #静态文件地址
index index.html index.htm; #入口文件
}
}

重启nginx

1
nginx -s reload

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第13章 编译打包)

Posted on 2017-08-16 | In 技术 , 课程笔记

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第13章 编译打包)

编译打包-项目编译打包及node服务测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
let express = require('express')
let config = require('./config/index')
let axios = require('axios')

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)
}).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)

app.use(express.static('./dist'))

let port = process.env.PORT || config.build.port

module.exports = app.listen(port, function (err) {
if (err) {
console.log(err)
return
}
console.log('Listening at https://localhost:' + port + '\n')
})

编译打包-路由组件实现懒加载

修改router

1
2
3
4
5
6
7
8
const Recommend = () => import('components/recommend/recommend')
const Singer = () => import('components/singer/singer')
const Rank = () => import('components/rank/rank')
const Search = () => import('components/search/search')
const SingerDetail = () => import('components/singer-detail/singer-detail')
const Disc = () => import('components/disc/disc')
const TopList = () => import('components/top-list/top-list')
const UserCenter = () => import('components/user-center/user-center')

总结

  1. 学习的价值不在于获得源码
  2. 组件化、模块化的本质都是分治(把复杂的项目拆分成一个个模块, 把模块拆分成组件, 把能复用的功能都抽象出js文件)
  3. 遇到问题要勇于面对、勤于思考
  4. 学会主动学习

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第12章 用户中心页面)

Posted on 2017-08-16 | In 技术 , 课程笔记

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第12章 用户中心页面)

用户中心页面布局和功能介绍

组件基本dom结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<transition name="slide">
<div class="user-center">
<!-- 返回按钮 start -->
<div class="back">
<i class="icon-back"></i>
</div>
<!-- 返回按钮 end -->
<!-- switch按钮 start -->
<div class="switches-wrapper">
</div>
<!-- switch按钮 end -->
<!-- 随机播放按钮 start -->
<div ref="playBtn" class="play-btn" @click="random">
<i class="icon-play"></i>
<span class="text">随机播放全部</span>
</div>
<!-- 随机播放按钮 end -->
<!-- 列表 start -->
<div class="list-wrapper" ref="listWrapper">
</div>
<!-- 列表 end -->
</div>
</transition>
</template>

用户中心页面收藏列表的Vuex数据设计和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// state
favoriteList: loadFavorite()

// mutation-type
export const SET_FAVORITE_LIST = 'SET_FAVORITE_LIST'

// mutation
[types.SET_FAVORITE_HISTORY](state, list) {
state.favoriteList = list
}

// getters
export const favoriteList = state => state.favoriteList

// actions
export const saveFavoriteList = function({ commit }, song) {
commit(types.SET_FAVORITE_LIST, saveFavorite(song))
}

export const deleteFavoriteList = function({ commit }, song) {
commit(types.SET_FAVORITE_LIST, deleteFavorite(song))
}

用户中心页面收藏歌曲功能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<template>
<transition name="slide">
<div class="user-center">
<!-- 返回按钮 start -->
<div class="back">
<i class="icon-back"></i>
</div>
<!-- 返回按钮 end -->
<!-- switch按钮 start -->
<div class="switches-wrapper">
<switches @switch="swtichItem" :switches="switches" :currentIndex="currentIndex"></switches>
</div>
<!-- switch按钮 end -->
<!-- 随机播放按钮 start -->
<div ref="playBtn" class="play-btn">
<i class="icon-play"></i>
<span class="text">随机播放全部</span>
</div>
<!-- 随机播放按钮 end -->
<!-- 列表 start -->
<div class="list-wrapper" ref="listWrapper">
<scroll ref="favoriteList" class="list-scroll" v-if="currentIndex===0" :data="favoriteList">
<div class="list-inner">
<song-list :songs="favoriteList" @select="selectSong"></song-list>
</div>
</scroll>
<scroll ref="playList" class="list-scroll" v-if="currentIndex===1" :data="playHistory">
<div class="list-inner">
<song-list :songs="playHistory" @select="selectSong"></song-list>
</div>
</scroll>
</div>
<!-- 列表 end -->
</div>
</transition>
</template>

<script>
import Switches from 'base/switches/switches'
import Scroll from 'base/scroll/scroll'
import SongList from 'base/song-list/song-list'
import Song from 'common/js/song'
import {mapGetters, mapActions} from 'vuex'

export default {
data() {
return {
currentIndex: 0,
switches: [
{name: '我喜欢的'},
{name: '最近听的'}
]
}
},
computed: {
...mapGetters([
'favoriteList',
'playHistory'
])
},
methods: {
swtichItem(index) {
this.currentIndex = index
},
selectSong(song) {
this.insertSong(new Song(song))
},
...mapActions([
'insertSong'
])
},
components: {
Switches,
Scroll,
SongList
}
}
</script>

用户中心页面剩余功能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<template>
<transition name="slide">
<div class="user-center">
<!-- 返回按钮 start -->
<div class="back" @click="back">
<i class="icon-back"></i>
</div>
<!-- 返回按钮 end -->
<!-- switch按钮 start -->
<div class="switches-wrapper">
<switches @switch="swtichItem" :switches="switches" :currentIndex="currentIndex"></switches>
</div>
<!-- switch按钮 end -->
<!-- 随机播放按钮 start -->
<div ref="playBtn" class="play-btn" @click="random">
<i class="icon-play"></i>
<span class="text">随机播放全部</span>
</div>
<!-- 随机播放按钮 end -->
<!-- 列表 start -->
<div class="list-wrapper" ref="listWrapper">
<scroll ref="favoriteList" class="list-scroll" v-if="currentIndex===0" :data="favoriteList">
<div class="list-inner">
<song-list :songs="favoriteList" @select="selectSong"></song-list>
</div>
</scroll>
<scroll ref="playlist" class="list-scroll" v-if="currentIndex===1" :data="playHistory">
<div class="list-inner">
<song-list :songs="playHistory" @select="selectSong"></song-list>
</div>
</scroll>
</div>
<!-- 列表 end -->
<div class="no-result-wrapper" v-show="noResult">
<no-result :title="noResultDesc"></no-result>
</div>
</div>
</transition>
</template>

<script>
import Switches from 'base/switches/switches'
import Scroll from 'base/scroll/scroll'
import SongList from 'base/song-list/song-list'
import NoResult from 'base/no-result/no-result'
import Song from 'common/js/song'
import {mapGetters, mapActions} from 'vuex'
import {playlistMixin} from 'common/js/mixin'

export default {
mixins: [playlistMixin],
data() {
return {
currentIndex: 0,
switches: [
{name: '我喜欢的'},
{name: '最近听的'}
]
}
},
computed: {
noResult() {
if (this.currentIndex === 0) {
return !this.favoriteList.length
} else {
return !this.playlist.length
}
},
noResultDesc() {
if (this.currentIndex === 0) {
return '暂无收藏歌曲'
} else {
return '你还没有听过歌曲'
}
},
...mapGetters([
'favoriteList',
'playHistory'
])
},
methods: {
handlePlaylist(playlist) {
const bottom = playlist.length > 0 ? '60px' : ''
this.$refs.listWrapper.style.bottom = bottom
this.$refs.$favoriteList && this.$refs.$favoriteList.refresh()
this.$refs.$playlist && this.$refs.$playlist.refresh()
},
swtichItem(index) {
this.currentIndex = index
},
selectSong(song) {
this.insertSong(new Song(song))
},
back() {
this.$router.back()
},
random() {
let list = this.currentIndex === 0 ? this.favoriteList : this.playHistory
if (list.length === 0) {
return
}
// 为什么用song的实例, song的实例上不仅有属性, 还有方法
list = list.map(song => new Song(song))
this.randomPlay({list})
},
...mapActions([
'insertSong',
'randomPlay'
])
},
components: {
Switches,
Scroll,
SongList,
NoResult
}
}
</script>

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第11章 歌曲列表组件)

Posted on 2017-08-16 | In 技术 , 课程笔记

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第11章 歌曲列表组件)

歌曲列表组件布局和功能介绍

添加playlist组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<template>
<transition name="list-fade">
<div class="playlist">
<div class="list-wrapper">
<div class="list-header">
<h1 class="title">
<i class="icon"></i>
<span class="text"></span>
<span class="clear"><i class="icon-clear"></i></span>
</h1>
</div>
<scroll ref="listContent" class="list-content">
<transition-group name="list" tag="ul">
<li ref="listItem" class="item">
<i class="current"></i>
<span class="text"></span>
<span class="like">
<i class="icon-not-favorite"></i>
</span>
<span class="delete">
<i class="icon-delete"></i>
</span>
</li>
</transition-group>
</scroll>
<div class="list-operate">
<div class="add">
<i class="icon-add"></i>
<span class="text">添加歌曲到队列</span>
</div>
</div>
<div class="list-close">
<span>关闭</span>
</div>
</div>
</div>
</transition>
</template>

<script>
export default {
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
@import "~common/stylus/mixin"
.playlist
position: fixed
left: 0
right: 0
top: 0
bottom: 0
z-index: 200
background-color: $color-background-d
&.list-fade-enter-active, &.list-fade-leave-active
transition: opacity 0.3s
.list-wrapper
transition: all 0.3s
&.list-fade-enter, &.list-fade-leave-to
opacity: 0
.list-wrapper
transform: translate3d(0, 100%, 0)
&.list-fade-enter
.list-wrapper
position: absolute
left: 0
bottom: 0
width: 100%
background-color: $color-highlight-background
.list-header
position: relative
padding: 20px 30px 10px 20px
.title
display: flex
align-items: center
.icon
margin-right: 10px
font-size: 30px
color: $color-theme-d
.text
flex: 1
font-size: $font-size-medium
color: $color-text-l
.clear
extend-click()
.icon-clear
font-size: $font-size-medium
color: $color-text-d
.list-content
max-height: 240px
overflow: hidden
.item
display: flex
align-items: center
height: 40px
padding: 0 30px 0 20px
overflow: hidden
&.list-enter-active, &.list-leave-active
transition: all 0.1s
&.list-enter, &.list-leave-to
height: 0
.current
flex: 0 0 20px
width: 20px
font-size: $font-size-small
color: $color-theme-d
.text
flex: 1
no-wrap()
font-size: $font-size-medium
color: $color-text-d
.like
extend-click()
margin-right: 15px
font-size: $font-size-small
color: $color-theme
.icon-favorite
color: $color-sub-theme
.delete
extend-click()
font-size: $font-size-small
color: $color-theme
.list-operate
width: 140px
margin: 20px auto 30px auto
.add
display: flex
align-items: center
padding: 8px 16px
border: 1px solid $color-text-l
border-radius: 100px
color: $color-text-l
.icon-add
margin-right: 5px
font-size: $font-size-small-s
.text
font-size: $font-size-small
.list-close
text-align: center
line-height: 50px
background: $color-background
font-size: $font-size-medium-x
color: $color-text-l
</style>

歌曲列表组件显示和隐藏的控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- 蒙层添加点击关闭事件 -->
<div class="playlist" v-show="showFlag" @click="hide">
<!-- 列表组件添加阻止冒泡事件 -->
<div class="list-wrapper" @click.stop>
...
</div>
</div>

<script>
export default {
data() {
return {
showFlag: false
}
},
methods: {
show() {
this.showFlag = true
},
hide() {
this.showFlag = false
}
}
}
</script>

歌曲列表组件播放列表的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
export default {
data() {
return {
showFlag: false
}
},
computed: {
...mapGetters([
'sequenceList',
'currentSong',
'playlist',
'mode'
])
},
methods: {
show() {
this.showFlag = true
setTimeout(() => {
this.$refs.listContent.refresh()
this.scrollToCurrent(this.currentSong)
}, 20)
},
hide() {
this.showFlag = false
},
getCurrentIcon(item) {
return this.currentSong.id === item.id ? 'icon-play' : ''
},
selectItem(item, index) {
// 播放模式是随机播放, 找到歌曲索引
if (this.mode === playMode.random) {
index = this.playlist.findIndex((song) => {
return song.id === item.id
})
}
this.setCurrentIndex(index)
this.setPlayingState(true)
},
scrollToCurrent(current) {
const index = this.sequenceList.findIndex((song) => {
return current.id === song.id
})
this.$refs.listContent.scrollToElement(this.$refs.listItem[index], 300)
},
...mapMutations({
'setCurrentIndex': 'SET_CURRENT_INDEX',
'setPlayingState': 'SET_PLAYING_STATE'
})
},
watch: {
currentSong(newSong, oldSong) {
if (!this.showFlag || newSong.id === oldSong.id) {
return
}
this.scrollToCurrent(newSong)
}
},
components: {
Scroll
}
}

添加action, 清空playlist

1
2
3
4
5
6
export const deleteSongList = function({ commit }) {
commit(types.SET_PLAYLIST, [])
commit(types.SET_SEQUENCE_LIST, [])
commit(types.SET_CURRENT_INDEX, -1)
commit(types.SET_PLAYING_STATE, false)
}

歌曲列表组件 playerMixin的抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
export const playlistMixin = {
computed: {
...mapGetters([
'playlist'
])
},
mounted() {
this.handlePlaylist(this.playlist)
},
activated() {
this.handlePlaylist(this.playlist)
},
watch: {
playlist(newVal) {
this.handlePlaylist(newVal)
}
},
methods: {
handlePlaylist() {
throw new Error('component must implement handlePlaylist method')
}
}
}

export const playerMixin = {
computed: {
iconMode() {
return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random'
},
...mapGetters([
'sequenceList',
'currentSong',
'playlist',
'mode'
])
},
methods: {
changeMode() {
const mode = (this.mode + 1) % 3
this.setPlayMode(mode)
let list = null
if (mode === playMode.random) {
list = shuffle(this.sequenceList)
} else {
list = this.sequenceList
}
this.resetCurrentIndex(list)
this.setPlaylist(list)
},
resetCurrentIndex(list) {
let index = list.findIndex((item) => {
return item.id === this.currentSong.id
})
this.setCurrentIndex(index)
},
...mapMutations({
setPlayingState: 'SET_PLAYING_STATE',
setCurrentIndex: 'SET_CURRENT_INDEX',
setPlayMode: 'SET_PLAY_MODE',
setPlaylist: 'SET_PLAYLIST'
})
}
}

好处

  1. 节省代码量
  2. 方便代码维护

歌曲列表组件add-song组件实现

组件add-song简单dom结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<transition name="slide">
<div class="add-song">
<!-- 添加歌曲header start -->
<div class="header">
<h1 class="title">添加歌曲到列表</h1>
<div class="close">
<i class="icon-close"></i>
</div>
</div>
<!-- 添加歌曲header end -->
<!-- 搜索框 start -->
<div class="search-box-wrapper">
</div>
<!-- 搜索框 end -->
<!-- 快速导航 start -->
<div class="shortcut">
</div>
<!-- 快速导航 end -->
<!-- 搜索结果 start -->
<div class="search-result">
</div>
<!-- 搜索结果 end -->
</div>
</transition>
</template>

添加mixin, search和add-song组件公用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export const searchMixin = {
data() {
return {
query: '',
refreshDelay: 120
}
},
computed: {
...mapGetters([
'searchHistory'
])
},
methods: {
blurInput() {
this.$refs.searchBox.blur()
},
saveSearch() {
this.saveSearchHistory(this.query)
},
onQueryChange(query) {
this.query = query
},
addQuery(query) {
this.$refs.searchBox.setQuery(query)
},
...mapActions([
'saveSearchHistory',
'deleteSearchHistory'
])
}
}

添加基础组件switches

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
<ul class="switches">
<li class="switch-item" v-for="(item,index) in switches" :class="{'active':currentIndex === index}"
@click="switchItem(index)">
<span>{{item.name}} </span>
</li>
</ul>
</template>

<script>
export default {
props: {
switches: {
type: Array,
default: []
},
currentIndex: {
type: Number,
default: 0
}
},
methods: {
switchItem(index) {
this.$emit('switch', index)
}
}
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
.switches
display: flex
align-items: center
width: 240px
margin: 0 auto
border: 1px solid $color-highlight-background
border-radius: 5px
.switch-item
flex: 1
padding: 8px
text-align: center
font-size: $font-size-medium
color: $color-text-d
&.active
background: $color-highlight-background
color: $color-text
</style>

实现最近播放

1
2
3
4
5
<scroll class="list-scroll" v-if="currentIndex===0" :data="playHistory">
<div class="list-inner">
<song-list :songs="playHistory" @select="selectSong"></song-list>
</div>
</scroll>

实现搜索历史

1
2
3
4
5
<scroll ref="searchList" class="list-scroll" v-if="currentIndex===1" :data="searchHistory">
<div class="list-inner">
<search-list :searches="searchHistory" @delete="deleteSearchHistory" @select="addQuery"></search-list>
</div>
</scroll>

歌曲列表组件top-list组件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<transition name="drop">
<div class="top-tip" v-show="showFlag" @click.stop="hide">
<slot></slot>
</div>
</transition>
</template>

<script>
export default {
props: {
delay: {
type: Number,
default: 2000
}
},
data() {
return {
showFlag: false
}
},
methods: {
show() {
this.showFlag = true
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.hide()
}, this.delay)
},
hide() {
this.showFlag = false
}
}
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
.top-tip
position: fixed
top: 0
width: 100%
z-index: 500
background: $color-dialog-background
&.drop-enter-active, &.drop-leave-active
transition: all 0.3s
&.drop-enter, &.drop-leave-to
transform: translate3d(0, -100%, 0)
</style>

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第十章 搜索页面开发)

Posted on 2017-08-16 | In 技术 , 课程笔记

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第十章 搜索页面开发)

搜索页面search-box组件开发

  1. 添加基础组件search-box

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    <template>
    <div class="search-box">
    <i class="icon-search"></i>
    <input ref="query" v-model="query" class="box" :placeholder="placeholder"/>
    <i @click="clear" v-show="query" class="icon-dismiss"></i>
    </div>
    </template>

    <script>
    import {debounce} from 'common/js/util'

    export default {
    props: {
    placeholder: {
    type: String,
    default: '搜索歌曲、歌手'
    }
    },
    data() {
    return {
    query: ''
    }
    },
    methods: {
    clear() {
    this.query = ''
    },
    setQuery(query) {
    this.query = query
    },
    blur() {
    this.$refs.query.blur()
    }
    },
    created() {
    // 监听query
    this.$watch('query', debounce((newQuery) => {
    this.$emit('query', newQuery)
    }, 200))
    }
    }
    </script>

    <style scoped lang="stylus" rel="stylesheet/stylus">
    @import "~common/stylus/variable"
    .search-box
    display: flex
    align-items: center
    box-sizing: border-box
    width: 100%
    padding: 0 6px
    height: 40px
    background: $color-highlight-background
    border-radius: 6px
    .icon-search
    font-size: 24px
    color: $color-background
    .box
    flex: 1
    margin: 0 5px
    line-height: 18px
    background: $color-highlight-background
    color: $color-text
    font-size: $font-size-medium
    &::placeholder
    color: $color-text-d
    .icon-dismiss
    font-size: 16px
    color: $color-background
    </style>
  2. 添加业务组件search

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    <template>
    <div class="search">
    <div class="search-box-wrapper">
    <search-box ref="searchBox"></search-box>
    </div>
    </div>
    </template>

    <script>
    import SearchBox from 'base/search-box/search-box'

    export default {
    components: {
    SearchBox
    }
    }
    </script>

    <style lang="stylus" rel="stylesheet/stylus">
    @import "~common/stylus/variable"
    @import "~common/stylus/mixin"
    .search
    .search-box-wrapper
    margin: 20px
    .shortcut-wrapper
    position: fixed
    top: 178px
    bottom: 0
    width: 100%
    .shortcut
    height: 100%
    overflow: hidden
    .hot-key
    margin: 0 20px 20px 20px
    .title
    margin-bottom: 20px
    font-size: $font-size-medium
    color: $color-text-l
    .item
    display: inline-block
    padding: 5px 10px
    margin: 0 20px 10px 0
    border-radius: 6px
    background: $color-highlight-background
    font-size: $font-size-medium
    color: $color-text-d
    .search-history
    position: relative
    margin: 0 20px
    .title
    display: flex
    align-items: center
    height: 40px
    font-size: $font-size-medium
    color: $color-text-l
    .text
    flex: 1
    .clear
    extend-click()
    .icon-clear
    font-size: $font-size-medium
    color: $color-text-d
    .search-result
    position: fixed
    width: 100%
    top: 178px
    bottom: 0
    </style>

搜索页面热门搜索数据抓取和应用

1
2
3
4
5
6
7
8
9
10
11
export function getHotKey() {
const url = 'https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg'

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

return jsonp(url, data, options)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<template>
<div class="search">
<div class="search-box-wrapper">
<search-box ref="searchBox"></search-box>
</div>
<div ref="shortcutWrapper" class="shortcut-wrapper" v-show="!query">
<div class="shortcut">
<div>
<div class="hot-key">
<h1 class="title">热门搜索</h1>
<ul>
<li @click="addQuery(item.k)" class="item" v-for="item in hotKey">
<span>{{item.k}}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
import SearchBox from 'base/search-box/search-box'
import {getHotKey} from 'api/search'
import {ERR_OK} from 'api/config'

export default {
data() {
return {
hotKey: []
}
},
created() {
this._getHotKey()
},
methods: {
addQuery(query) {
this.$refs.searchBox.setQuery(query)
},
_getHotKey() {
getHotKey().then((res) => {
if (res.code === ERR_OK) {
this.hotKey = res.data.hotkey.slice(0, 10)
}
})
}
},
watch: {
query(newQuery) {
if (!newQuery) {
setTimeout(() => {
this.$refs.shortcut.refresh()
}, 20)
}
}
},
components: {
SearchBox
}
}
</script>

搜索页面suggest组件开发

  1. 添加接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    export function search(query, page, zhida, perpage) {
    const url = 'https://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp'

    const data = Object.assign({}, commonParams, {
    w: query,
    p: page,
    perpage,
    n: perpage,
    catZhida: zhida ? 1 : 0,
    zhidaqu: 1,
    t: 0,
    flag: 1,
    ie: 'utf-8',
    sem: 1,
    aggr: 0,
    remoteplace: 'txt.mqq.all',
    uin: 0,
    needNewCode: 1,
    platform: 'h5'
    })

    return jsonp(url, data, options)
    }
  2. 实现上拉刷新, 扩展scroll组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // props
    pullup: {
    type: Boolean,
    default: false
    }
    // methods
    // 当滚动结束时离底部还有50px时, 派发事件scrollToEnd
    if (this.pullup) {
    this.scroll.on('scrollEnd', () => {
    if (this.scroll.y <= (this.scroll.maxScrollY + 50)) {
    this.$emit('scrollToEnd')
    }
    })
    }
  3. suggest中添加方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    searchMore() {
    if (!this.hasMore) {
    return
    }
    this.page++
    search(this.query, this.page, this.showSinger, perpage).then((res) => {
    if (res.code === ERR_OK) {
    this.result = this.result.concat(this._genResult(res.data))
    this._checkMore(res.data)
    }
    })
    },
    _checkMore(data) {
    const song = data.song
    if (!song.list.length || (song.curnum + song.curpage * perpage) >= song.totalnum) {
    this.hasMore = false
    }
    }
  4. 添加actions

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    export const insertSong = function ({commit, state}, song) {
    let playlist = state.playlist.slice()
    let sequenceList = state.sequenceList.slice()
    let currentIndex = state.currentIndex
    // 记录当前歌曲
    let currentSong = playlist[currentIndex]
    // 查找当前列表中是否有待插入的歌曲并返回其索引
    let fpIndex = findIndex(playlist, song)
    // 因为是插入歌曲,所以索引+1
    currentIndex++
    // 插入这首歌到当前索引位置
    playlist.splice(currentIndex, 0, song)
    // 如果已经包含了这首歌
    if (fpIndex > -1) {
    // 如果当前插入的序号大于列表中的序号
    if (currentIndex > fpIndex) {
    playlist.splice(fpIndex, 1)
    currentIndex--
    } else {
    playlist.splice(fpIndex + 1, 1)
    }
    }

    // 应该插入的位置
    let currentSIndex = findIndex(sequenceList, currentSong) + 1

    let fsIndex = findIndex(sequenceList, song)

    sequenceList.splice(currentSIndex, 0, song)

    if (fsIndex > -1) {
    if (currentSIndex > fsIndex) {
    sequenceList.splice(fsIndex, 1)
    } else {
    sequenceList.splice(fsIndex + 1, 1)
    }
    }

    commit(types.SET_PLAYLIST, playlist)
    commit(types.SET_SEQUENCE_LIST, sequenceList)
    commit(types.SET_CURRENT_INDEX, currentIndex)
    commit(types.SET_FULL_SCREEN, true)
    commit(types.SET_PLAYING_STATE, true)
    }
  5. 添加通用组件no-result

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <template>
    <div class="no-result">
    <div class="no-result-icon"></div>
    <p class="no-result-text">{{title}}</p>
    </div>
    </template>

    <script type="text/ecmascript-6">
    export default {
    props: {
    title: {
    type: String,
    default: ''
    }
    }
    }
    </script>

    <style scoped lang="stylus" rel="stylesheet/stylus">
    @import "~common/stylus/variable"
    @import "~common/stylus/mixin"
    .no-result
    text-align: center
    .no-result-icon
    width: 86px
    height: 90px
    margin: 0 auto
    bg-image('no-result')
    background-size: 86px 90px
    .no-result-text
    margin-top: 30px
    font-size: $font-size-medium
    color: $color-text-d
    </style>
  6. 在common/js/util.js中添加节流函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    export function debounce(func, delay) {
    let timer

    return function (...args) {
    if (timer) {
    clearTimeout(timer)
    }
    timer = setTimeout(() => {
    func.apply(this, args)
    }, delay)
    }
    }

搜索页面搜索结果保存功能实现

  1. suggest组件selectItem方法派发事件

    1
    this.$emit('select', item)
  2. 添加commom/js/cache.js 缓存操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function insertArray(arr, val, compare, maxLen) {
    const index = arr.findIndex(compare)
    if (index === 0) {
    return
    }
    // 存在元素删除后插入, 没有直接插入
    if (index > 0) {
    arr.splice(index, 1)
    }
    arr.unshift(val)
    if (maxLen && arr.length > maxLen) {
    arr.pop()
    }
    }
    export function saveSearch(query) {
    let searches = storage.get(SEARCH_KEY, [])
    insertArray(searches, query, (item) => {
    return item === query
    }, SEARCH_MAX_LEN)
    storage.set(SEARCH_KEY, searches)
    return searches
    }
  3. 添加actions

    1
    2
    3
    export const saveSearchHistory = function ({commit}, query) {
    commit(types.SET_SEARCH_HISTORY, saveSearch(query))
    }

搜索页面search-list 组件功能实现

  1. 添加组件search-list

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    <template>
    <div class="search-list" v-show="searches.length">
    <ul>
    <li class="search-item" @click="selectItem(item)" v-for="item in searches">
    <span class="text">{{item}}</span>
    <span class="icon" @click.stop="deleteOne(item)">
    <i class="icon-delete"></i>
    </span>
    </li>
    </ul>
    </div>
    </template>

    <script>
    export default {
    props: {
    searches: {
    type: Array,
    default: []
    }
    },
    methods: {
    selectItem(item) {
    this.$emit('select', item)
    },
    deleteOne(item) {
    this.$emit('delete', item)
    }
    }
    }
    </script>

    <style scoped lang="stylus" rel="stylesheet/stylus">
    @import "~common/stylus/variable"
    .search-list
    .search-item
    display: flex
    align-items: center
    height: 40px
    .text
    flex: 1
    color: $color-text-l
    .icon
    extend-click()
    .icon-delete
    font-size: $font-size-small
    color: $color-text-d
    </style>
  2. cache添加方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function deleteFromArray(arr, compare) {
    const index = arr.findIndex(compare)
    if (index > -1) {
    arr.splice(index, 1)
    }
    }
    export function deleteSearch(query) {
    let searches = storage.get(SEARCH_KEY, [])
    deleteFromArray(searches, (item) => {
    return item === query
    })
    storage.set(SEARCH_KEY, searches)
    return searches
    }
  3. 添加action

    1
    2
    3
    export const deleteSearchHistory = function ({commit}, query) {
    commit(types.SET_SEARCH_HISTORY, deleteSearch(query))
    }
  4. 添加cache

    1
    2
    3
    4
    export function clearSearch() {
    storage.remove(SEARCH_KEY)
    return []
    }
  5. 添加actions

    1
    2
    3
    export const clearSearchHistory = function ({commit}) {
    commit(types.SET_SEARCH_HISTORY, clearSearch())
    }

搜索页面confirm 组件功能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<template>
<transition name="confirm-fade">
<div class="confirm" v-show="showFlag" @click.stop>
<div class="confirm-wrapper">
<div class="confirm-content">
<p class="text">{{text}}</p>
<div class="operate">
<div @click="cancel" class="operate-btn left">{{cancelBtnText}}</div>
<div @click="confirm" class="operate-btn">{{confirmBtnText}}</div>
</div>
</div>
</div>
</div>
</transition>
</template>

<script>
export default {
props: {
text: {
type: String,
default: ''
},
confirmBtnText: {
type: String,
default: '确定'
},
cancelBtnText: {
type: String,
default: '取消'
}
},
data() {
return {
showFlag: false
}
},
methods: {
show() {
this.showFlag = true
},
hide() {
this.showFlag = false
},
cancel() {
this.hide()
this.$emit('cancel')
},
confirm() {
this.hide()
this.$emit('confirm')
}
}
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
.confirm
position: fixed
left: 0
right: 0
top: 0
bottom: 0
z-index: 998
background-color: $color-background-d
&.confirm-fade-enter-active
animation: confirm-fadein 0.3s
.confirm-content
animation: confirm-zoom 0.3s
.confirm-wrapper
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
z-index: 999
.confirm-content
width: 270px
border-radius: 13px
background: $color-highlight-background
.text
padding: 19px 15px
line-height: 22px
text-align: center
font-size: $font-size-large
color: $color-text-l
.operate
display: flex
align-items: center
text-align: center
font-size: $font-size-large
.operate-btn
flex: 1
line-height: 22px
padding: 10px 0
border-top: 1px solid $color-background-d
color: $color-text-d
&.left
border-right: 1px solid $color-background-d
@keyframes confirm-fadein
0%
opacity: 0
100%
opacity: 1
@keyframes confirm-zoom
0%
transform: scale(0)
50%
transform: scale(1.1)
100%
transform: scale(1)
</style>

搜索页面剩余功能实现

给滚动组件传值, 一个复合属性

1
2
3
4
5
computed: {
shortcut() {
return this.hotKey.concat(this.searchHistory)
},
}

10个JavaScript难点

Posted on 2017-08-10 | In 技术

10个JavaScript难点

本文转载自:segmentfault
译者:fundebug
链接:https://segmentfault.com/a/1190000010371988#articleHeader10
原文:10 JavaScript concepts every Node.js programmer must master

1. 立即执行函数

立即执行函数,即Immediately Invoked Function Expression (IIFE),正如它的名字,就是创建函数的同时立即执行。它没有绑定任何事件,也无需等待任何异步操作:

1
2
3
4
(function() {
// 代码
// ...
})();

function(){…}是一个匿名函数,包围它的一对括号将其转换为一个表达式,紧跟其后的一对括号调用了这个函数。立即执行函数也可以理解为立即调用一个匿名函数。立即执行函数最常见的应用场景就是:将var变量的作用域限制于你们函数内,这样可以避免命名冲突。

2. 闭包

对于闭包(closure),当外部函数返回之后,内部函数依然可以访问外部函数的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function f1()
{
var N = 0; // N是f1函数的局部变量

function f2() // f2是f1函数的内部函数,是闭包
{
N += 1; // 内部函数f2中使用了外部函数f1中的变量N
console.log(N);
}

return f2;
}

var result = f1();

result(); // 输出1
result(); // 输出2
result(); // 输出3

代码中,外部函数f1只执行了一次,变量N设为0,并将内部函数f2赋值给了变量result。由于外部函数f1已经执行完毕,其内部变量N应该在内存中被清除,然而事实并不是这样:我们每次调用result的时候,发现变量N一直在内存中,并且在累加。为什么呢?这就是闭包的神奇之处了!

使用闭包定义私有变量

通常,JavaScript开发者使用下划线作为私有变量的前缀。但是实际上,这些变量依然可以被访问和修改,并非真正的私有变量。这时,使用闭包可以定义真正的私有变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Product() {

var name;

this.setName = function(value) {
name = value;
};

this.getName = function() {
return name;
};
}

var p = new Product();
p.setName("Fundebug");

console.log(p.name); // 输出undefined
console.log(p.getName()); // 输出Fundebug

代码中,对象p的的name属性为私有属性,使用p.name不能直接访问。

4. prototype

每个JavaScript构造函数都有一个prototype属性,用于设置所有实例对象需要共享的属性和方法。prototype属性不能列举。JavaScript仅支持通过prototype属性进行继承属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Rectangle(x, y)
{
this._length = x;
this._breadth = y;
}

Rectangle.prototype.getDimensions = function()
{
return {
length: this._length,
breadth: this._breadth
};
};

var x = new Rectangle(3, 4);
var y = new Rectangle(4, 3);

console.log(x.getDimensions()); // { length: 3, breadth: 4 }
console.log(y.getDimensions()); // { length: 4, breadth: 3 }

代码中,x和y都是构造函数Rectangle创建的对象实例,它们通过prototype继承了getDimensions方法。

5. 模块化

JavaScript并非模块化编程语言,至少ES6落地之前都不是。然而对于一个复杂的Web应用,模块化编程是一个最基本的要求。这时,可以使用立即执行函数来实现模块化,正如很多JS库比如jQuery以及我们Fundebug都是这样实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var module = (function() {
var N = 5;

function print(x) {
console.log("The result is: " + x);
}

function add(a) {
var x = a + N;
print(x);
}

return {
description: "This is description",
add: add
};
})();


console.log(module.description); // 输出"this is description"

module.add(5); // 输出“The result is: 10”

所谓模块化,就是根据需要控制模块内属性与方法的可访问性,即私有或者公开。在代码中,module为一个独立的模块,N为其私有属性,print为其私有方法,decription为其公有属性,add为其共有方法。

6. 变量提升

JavaScript会将所有变量和函数声明移动到它的作用域的最前面,这就是所谓的变量提升(Hoisting)。也就是说,无论你在什么地方声明变量和函数,解释器都会将它们移动到作用域的最前面。因此我们可以先使用变量和函数,而后声明它们。

但是,仅仅是变量声明被提升了,而变量赋值不会被提升。如果你不明白这一点,有时则会出错:

1
2
3
console.log(y);  // 输出undefined

y = 2; // 初始化y

上面的代码等价于下面的代码:

1
2
3
4
5
var y;  // 声明y

console.log(y); // 输出undefined

y = 2; // 初始化y

为了避免BUG,开发者应该在每个作用域开始时声明变量和函数。

7. 柯里化

柯里化,即Currying,可以是函数变得更加灵活。我们可以一次性传入多个参数调用它;也可以只传入一部分参数来调用它,让它返回一个函数去处理剩下的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
var add = function(x) {
return function(y) {
return x + y;
};
};

console.log(add(1)(1)); // 输出2

var add1 = add(1);
console.log(add1(1)); // 输出2

var add10 = add(10);
console.log(add10(1)); // 输出11

代码中,我们可以一次性传入2个1作为参数add(1)(1),也可以传入1个参数之后获取add1与add10函数,这样使用起来非常灵活。

8. apply, call与bind方法

JavaScript开发者有必要理解apply、call与bind方法的不同点。它们的共同点是第一个参数都是this,即函数运行时依赖的上下文。

三者之中,call方法是最简单的,它等价于指定this值调用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var user = {
name: "Rahul Mhatre",
whatIsYourName: function() {
console.log(this.name);
}
};

user.whatIsYourName(); // 输出"Rahul Mhatre",

var user2 = {
name: "Neha Sampat"
};

user.whatIsYourName.call(user2); // 输出"Neha Sampat"

apply方法与call方法类似。两者唯一的不同点在于,apply方法使用数组指定参数,而call方法每个参数单独需要指定:

  • apply(thisArg, [argsArray])
  • call(thisArg, arg1, arg2, …)
1
2
3
4
5
6
7
8
9
10
11
12
13
var user = {
greet: "Hello!",
greetUser: function(userName) {
console.log(this.greet + " " + userName);
}
};

var greet1 = {
greet: "Hola"
};

user.greetUser.call(greet1, "Rahul"); // 输出"Hola Rahul"
user.greetUser.apply(greet1, ["Rahul"]); // 输出"Hola Rahul"

使用bind方法,可以为函数绑定this值,然后作为一个新的函数返回:

1
2
3
4
5
6
7
8
9
10
11
12
var user = {
greet: "Hello!",
greetUser: function(userName) {
console.log(this.greet + " " + userName);
}
};

var greetHola = user.greetUser.bind({greet: "Hola"});
var greetBonjour = user.greetUser.bind({greet: "Bonjour"});

greetHola("Rahul") // 输出"Hola Rahul"
greetBonjour("Rahul") // 输出"Bonjour Rahul"

9. Memoization

Memoization用于优化比较耗时的计算,通过将计算结果缓存到内存中,这样对于同样的输入值,下次只需要中内存中读取结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function memoizeFunction(func)
{
var cache = {};
return function()
{
var key = arguments[0];
if (cache[key])
{
return cache[key];
}
else
{
var val = func.apply(this, arguments);
cache[key] = val;
return val;
}
};
}


var fibonacci = memoizeFunction(function(n)
{
return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(100)); // 输出354224848179262000000
console.log(fibonacci(100)); // 输出354224848179262000000

代码中,第2次计算fibonacci(100)则只需要在内存中直接读取结果。

10. 函数重载

所谓函数重载(method overloading),就是函数名称一样,但是输入输出不一样。或者说,允许某个函数有各种不同输入,根据不同的输入,返回不同的结果。凭直觉,函数重载可以通过if…else或者switch实现,这就不去管它了。jQuery之父John Resig提出了一个非常巧(bian)妙(tai)的方法,利用了闭包。

从效果上来说,people对象的find方法允许3种不同的输入: 0个参数时,返回所有人名;1个参数时,根据firstName查找人名并返回;2个参数时,根据完整的名称查找人名并返回。

难点在于,people.find只能绑定一个函数,那它为何可以处理3种不同的输入呢?它不可能同时绑定3个函数find0,find1与find2啊!这里的关键在于old属性。

由addMethod函数的调用顺序可知,people.find最终绑定的是find2函数。然而,在绑定find2时,old为find1;同理,绑定find1时,old为find0。3个函数find0,find1与find2就这样通过闭包链接起来了。

根据addMethod的逻辑,当f.length与arguments.length不匹配时,就会去调用old,直到匹配为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function addMethod(object, name, f)
{  
var old = object[name];  
object[name] = function()
{
// f.length为函数定义时的参数个数
// arguments.length为函数调用时的参数个数    
if (f.length === arguments.length)
{  
return f.apply(this, arguments);    
}
else if (typeof old === "function")
{
return old.apply(this, arguments);    
}  
};
}


// 不传参数时,返回所有name
function find0()
{  
return this.names;
}


// 传一个参数时,返回firstName匹配的name
function find1(firstName)
{  
var result = [];  
for (var i = 0; i < this.names.length; i++)
{    
if (this.names[i].indexOf(firstName) === 0)
{      
result.push(this.names[i]);    
}  
}  
return result;
}


// 传两个参数时,返回firstName和lastName都匹配的name
function find2(firstName, lastName)
{ 
var result = [];  
for (var i = 0; i < this.names.length; i++)
{    
if (this.names[i] === (firstName + " " + lastName))
{      
result.push(this.names[i]);    
}  
}  
return result;
}


var people = {  
names: ["Dean Edwards", "Alex Russell", "Dean Tom"]
};


addMethod(people, "find", find0);
addMethod(people, "find", find1);
addMethod(people, "find", find2);


console.log(people.find()); // 输出["Dean Edwards", "Alex Russell", "Dean Tom"]
console.log(people.find("Dean")); // 输出["Dean Edwards", "Dean Tom"]
console.log(people.find("Dean", "Edwards")); // 输出["Dean Edwards"]

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第九章 排行榜及详情页开发)

Posted on 2017-08-10 | In 技术 , 课程笔记

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第九章 排行榜及详情页开发)

排行页面布局介绍及排行榜数据抓取

添加api/rank.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import jsonp from 'common/js/jsonp'
import {commonParams, options} from './config'

export function getTopList() {
const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg'

const data = Object.assign({}, commonParams, {
needNewCode: 1,
platform: 'h5'
})

return jsonp(url, data, options)
}

排行页排行榜数据应用

新建组件rank

榜单详情页布局介绍及Vuex实现路由数据通讯

新建组件top-list

榜单详情页数据抓取和应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function getMusicList(topid) {
const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_toplist_cp.fcg'

const data = Object.assign({}, commonParams, {
topid,
needNewCode: 1,
uin: 0,
tpl: 3,
page: 'detail',
type: 'top',
platform: 'h5'
})

return jsonp(url, data, options)
}

带排行的song-list组件扩展和应用

  1. 添加dom

    1
    2
    3
    <div class="rank" v-show="rank">
    <span :class="getRankCls(index)">{{getRankText(index)}}</span>
    </div>
  2. 添加props

    1
    2
    3
    4
    rank: {
    type: Boolean,
    default: false
    }
  3. 添加methods

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    getRankCls(index) {
    if (index <= 2) {
    return `icon icon${index}`
    } else {
    return 'text'
    }
    },
    getRankText(index) {
    if (index > 2) {
    return index + 1
    }
    }
  4. music-list添加props

    1
    2
    3
    4
    rank: {
    type: Boolean,
    default: false
    }

添加hexo Next主题自定样式

Posted on 2017-08-07

添加hexo Next主题自定样式

添加自定义样式主要有两种

  1. 添加自定义css
  2. 添加自定义内置标签

添加自定义样式

在themes/next/source/css/_custom/中添加样式文件, 例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.label{
font-family: Consolas;
display: inline;
color: #fff;
padding: 1px 3px 2px 3px;
margin: 0px 2px 0px 2px;
border-radius: 2px;
}
.label-red{
background: #d9534f;
}
.label-green{
background: #5cb85c;
}
.label-blue{
background: #428bca;
}
.label-sky{
background: #5bc0de;
}
.label-orange{
background: #f0ad4e;
}

在themes/next/source/css/main.styl用引入样式文件

1
@import "_custom/custom";

在文章中使用

1
<span class="label label-red">New</span>

添加自定义内置标签

在themes/next/scripts/tags中添加文件, 例如

1
2
3
4
5
function bscallOut (args, content) {
return '<div class="note ' + args.join(' ') + '">' + hexo.render.renderSync({text: content, engine: 'markdown'}).trim() + '</div>';
}

hexo.extend.tag.register('note', bscallOut, {ends: true});

在文章中使用

1
2
3
4
5
{% note [class] %}
Any content (support inline tags too).
{% endnote %}

// [class] : default | primary | success | info | warning | danger

hexo完整的标签列表

Next主题内置的Bootstrap Callout

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第八章 歌单页面开发)

Posted on 2017-08-07 | In 技术 , 课程笔记

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第八章 歌单页面开发)

歌单详情页布局介绍及Vuex实现路由数据通讯

歌单详情页数据抓取

api/recommend.js 添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function getSongList(disstid) {
const url = 'https://c.y.qq.com/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg'

const data = Object.assign({}, commonParams, {
disstid,
type: 1,
json: 1,
utf8: 1,
onlysong: 0,
platform: 'yqq',
hostUin: 0,
needNewCode: 0
})

return jsonp(url, data, options)
}

歌单详情页数据的处理和应用

1
2
3
4
5
6
7
8
9
_normalizeSongs(list) {
let ret = []
list.forEach((musicData) => {
if (musicData.songid && musicData.albummid) {
ret.push(createSong(musicData))
}
})
return ret
}

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第七章 播放器内置组件开发)

Posted on 2017-08-07 | In 技术 , 课程笔记

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第七章 播放器内置组件开发)

播放器Vuex数据设计

  1. state
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const state = {
    singer: {},
    playing: false,
    fullScreen: false,
    playlist: [],
    sequenceList: [],
    mode: playMode.sequence,
    currentIndex: -1
    }

state只保留最基础的数据, 计算属性放在getters中

  1. getters

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    export const playing = state => state.playing

    export const fullScreen = state => state.fullScreen

    export const playlist = state => state.playlist

    export const sequenceList = state => state.sequenceList

    export const mode = state => state.mode

    export const currentIndex = state => state.currentIndex

    export const currentSong = (state) => {
    return state.playlist[state.currentIndex] || {}
    }
  2. mutation-types

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    export const SET_PLAYING_STATE = 'SET_PLAYING_STATE'

    export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'

    export const SET_PLAYLIST = 'SET_PLAYLIST'

    export const SET_SEQUENCE_LIST = 'SET_SEQUENCE_LIST'

    export const SET_PLAY_MODE = 'SET_PLAY_MODE'

    export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX'
  3. mutations

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import * as types from './mutation-types'

    const matutaions = {
    [types.SET_SINGER](state, singer) {
    state.singer = singer
    },
    [types.SET_PLAYING_STATE](state, flag) {
    state.playing = flag
    },
    [types.SET_FULL_SCREEN](state, flag) {
    state.fullScreen = flag
    },
    [types.SET_PLAYLIST](state, list) {
    state.playlist = list
    },
    [types.SET_SEQUENCE_LIST](state, list) {
    state.sequenceList = list
    },
    [types.SET_PLAY_MODE](state, mode) {
    state.mode = mode
    },
    [types.SET_CURRENT_INDEX](state, index) {
    state.currentIndex = index
    }
    }

    export default matutaions

actions的操作通常有两种

  1. 异步操作
  2. 对mutation的封装

播放器Vuex的相关应用

  1. 新建组件player.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <template>
    <div class="player">
    <div class="normal-player">
    播放器
    </div>
    <div class="mini-player"></div>
    </div>
    </template>

    <script>
    export default {}
    </script>

    <style scoped lang="stylus" rel="stylesheet/stylus">

    </style>
  2. 在App.vue中注册组件

  3. 在song-list.vue 中派发事件

    1
    2
    3
    selectItem(song, index) {
    this.$emit('select', item, index)
    }
  4. 在actions定义动作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import * as types from './mutation-types'

    export const selectPlay = function ({commit, state}, {list, index}) {
    commit(types.SET_SEQUENCE_LIST, list)
    commit(types.SET_PLAYLIST, randomList)
    commit(types.SET_CURRENT_INDEX, index)
    commit(types.SET_FULL_SCREEN, true)
    commit(types.SET_PLAYING_STATE, true)
    }
  5. 在music-list.vue 接收事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    selectItem(item, index) {
    this.selectPlay({
    list: this.songs,
    index
    })
    },
    ...mapActions([
    'selectPlay'
    ])

播放器基础样式及歌曲数据的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<template>
<div class="music-list">
<div class="back" @click="back">
<i class="icon-back"></i>
</div>
<h1 class="title" v-html="title"></h1>
<div class="bg-image" :style="bgStyle" ref="bgImage">
<div class="play-wrapper">
<div ref="playBtn" v-show="songs.length>0" class="play">
<i class="icon-play"></i>
<span class="text">随机播放全部</span>
</div>
</div>
<div class="filter" ref="filter"></div>
</div>
<div class="bg-layer" ref="layer"></div>
<scroll :data="songs" @scroll="scroll" :listen-scroll="listenScroll" :probe-type="probeType" class="list" ref="list">
<div class="song-list-wrapper">
<song-list @select="selectItem" :songs="songs"></song-list>
</div>
<div v-show="!songs.length" class="loading-container">
<loading></loading>
</div>
</scroll>
</div>
</template>
<script>
import Scroll from 'base/scroll/scroll'
import Loading from 'base/loading/loading'
import SongList from 'base/song-list/song-list'
import { prefixStyle } from 'common/js/dom'
import { mapActions } from 'vuex'

const RESERVED_HEIGHT = 40
const transform = prefixStyle('transform')
const backdrop = prefixStyle('backdrop-filter')

export default {
props: {
bgImage: {
type: String,
default: ''
},
songs: {
type: Array,
default: []
},
title: {
type: String,
default: ''
}
},
data() {
return {
scrollY: 0
}
},
computed: {
bgStyle() {
return `background-image:url(${this.bgImage})`
}
},
created() {
this.probeType = 3
this.listenScroll = true
},
mounted() {
this.imageHeight = this.$refs.bgImage.clientHeight
this.minTransalteY = -this.imageHeight + RESERVED_HEIGHT
this.$refs.list.$el.style.top = `${this.imageHeight}px`
},
methods: {
scroll(pos) {
this.scrollY = pos.y
},
back() {
this.$router.back()
},
// 子组件行为和本身相关, 不要依赖外部组件使用定义子组件, 子组件行为和本身相关, 传item值是子组件点击时该做的事
selectItem(item, index) {
this.selectPlay({
list: this.songs,
index
})
},
...mapActions([
'selectPlay'
])
},
watch: {
scrollY(newVal) {
let translateY = Math.max(this.minTransalteY, newVal)
let scale = 1
let zIndex = 0
let blur = 0
// 计算图片放大比例
const percent = Math.abs(newVal / this.imageHeight)
if (newVal > 0) {
scale = 1 + percent
zIndex = 10
} else {
// 计算模糊比例
blur = Math.min(20, percent * 20)
}

this.$refs.layer.style[transform] = `translate3d(0,${translateY}px,0)`
this.$refs.filter.style[backdrop] = `blur(${blur}px)`
if (newVal < this.minTransalteY) {
// 向上滑动超过minTransalteY, 改变图片zIndex和高度, 按钮消失
zIndex = 10
this.$refs.bgImage.style.paddingTop = 0
this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px`
this.$refs.playBtn.style.display = 'none'
} else {
// 反之, 重置
this.$refs.bgImage.style.paddingTop = '70%'
this.$refs.bgImage.style.height = 0
this.$refs.playBtn.style.display = ''
}
this.$refs.bgImage.style[transform] = `scale(${scale})`
this.$refs.bgImage.style.zIndex = zIndex
}
},
components: {
Scroll,
Loading,
SongList
}
}

播放器展开收起动画

第三方库: create-keyframe-animation

1
2
3
4
5
6
7
8
<transition name="normal"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@after-leave="afterLeave">
...normal
</transition>
<script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 导入插件
import animations from 'create-keyframe-animation'
import {prefixStyle} from 'common/js/dom'

const transform = prefixStyle('transform')

// 添加methods方法
/**
* 动画进入
* @param {object} el dom
* @param {Function} done 回调函数, 即afterEnter
*/
enter(el, done) {
const {x, y, scale} = this._getPosAndScale()

let animation = {
0: {
transform: `translate3d(${x}px,${y}px,0) scale(${scale})`
},
60: {
transform: `translate3d(0,0,0) scale(1.1)`
},
100: {
transform: `translate3d(0,0,0) scale(1)`
}
}

animations.registerAnimation({
name: 'move',
animation,
presets: {
duration: 400,
easing: 'linear'
}
})

animations.runAnimation(this.$refs.cdWrapper, 'move', done)
},
/**
* 注销动画
*/
afterEnter() {
animations.unregisterAnimation('move')
this.$refs.cdWrapper.style.animation = ''
},
/**
* 动画离开
* @param {object} el dom
* @param {Function} done 回调函数, 即afterLeave
*/
leave(el, done) {
this.$refs.cdWrapper.style.transition = 'all 0.4s'
const {x, y, scale} = this._getPosAndScale()
this.$refs.cdWrapper.style[transform] = `translate3d(${x}px,${y}px,0) scale(${scale})`
this.$refs.cdWrapper.addEventListener('transitionend', done)
},
afterLeave() {
this.$refs.cdWrapper.style.transition = ''
this.$refs.cdWrapper.style[transform] = ''
},
/**
* 计算相对位置和缩放比例
* @return {object} 宽度, 高度, 缩放比例
*/
_getPosAndScale() {
const targetWidth = 40
const paddingLeft = 40
const paddingBottom = 30
const paddingTop = 80
const width = window.innerWidth * 0.8
const scale = targetWidth / width
const x = -(window.innerWidth / 2 - paddingLeft)
const y = window.innerHeight - paddingTop - width / 2 - paddingBottom
return {
x,
y,
scale
}
},

播放器歌曲播放功能实现

  1. 添加radio

    1
    <audio ref="audio" :src="currentSong.url"></audio>
  2. 添加methods: togglePlaying, watch: currentSong,
    playing

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // methods
    togglePlaying() {
    this.setPlayingState(!this.playing)
    }

    // watch
    currentSong(newSong, oldSong) {
    this.$nextTick(() => {
    this.$refs.audio.play()
    })
    },
    playing(newPlaying) {
    const audio = this.$refs.audio
    this.$nextTick(() => {
    newPlaying ? audio.play() : audio.pause()
    })
    }
  3. 添加计算属性, 计算图标

    1
    2
    3
    4
    5
    6
    playIcon() {
    return this.playing ? 'icon-pause' : 'icon-play'
    },
    miniIcon() {
    return this.playing ? 'icon-pause-mini' : 'icon-play-mini'
    }
  4. 添加计算属性, 计算图标是否旋转

    1
    2
    3
    cdCls() {
    return this.playing ? 'play' : 'play pause'
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.cd {
&.play {
animation: rotate 20s linear infinite
}
&.pause {
animation-play-state: paused
}
}

@keyframes rotate {
0% {
transform: rotate(0)
}
100% {
transform: rotate(360deg)
}
}

播放器歌曲前进后退功能实现

  1. 添加歌曲上一首, 下一首事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 播放下一首歌
    next() {
    if (!this.songReady) {
    return
    }
    let index = this.currentIndex + 1
    if (index === this.playlist.length) {
    index = 0
    }
    this.setCurrentIndex(index)
    if (!this.playing) {
    this.togglePlaying()
    }
    this.songReady = false
    },
    // 播放上一首歌
    prev() {
    if (!this.songReady) {
    return
    }
    let index = this.currentIndex - 1
    if (index === -1) {
    index = this.playlist.length - 1
    }
    this.setCurrentIndex(index)
    if (!this.playing) {
    this.togglePlaying()
    }
    this.songReady = false
    },
    ready() {
    this.songReady = true
    }

播放器播放时间获取和更新

添加事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取播放时间
updateTime(e) {
this.currentTime = e.target.currentTime
}

// 补位
_pad(num, n = 2) {
let len = num.toString().length
while (len < n) {
num = '0' + num
len++
}
return num
},
// 格式化
format(interval) {
interval = interval | 0
const minute = interval / 60 | 0
const second = this._pad(interval % 60)
return `${minute}:${second}`
}

播放器progress-bar进度条组件实现

  1. palyer.vue 添加计算属性

    1
    2
    3
    percent() {
    return this.currentTime / this.currentSong.duration
    }
  2. 添加基础组件progress-bar.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    <template>
    <div class="progress-bar" ref="progressBar" @click="progressClick">
    <div class="bar-inner">
    <div class="progress" ref="progress"></div>
    <div class="progress-btn-wrapper" ref="progressBtn"
    @touchstart.prevent="progressTouchStart"
    @touchmove.prevent="progressTouchMove"
    @touchend="progressTouchEnd"
    >
    <div class="progress-btn"></div>
    </div>
    </div>
    </div>
    </template>

    <script>
    import {prefixStyle} from 'common/js/dom'

    const progressBtnWidth = 16
    const transform = prefixStyle('transform')

    export default {
    props: {
    percent: {
    type: Number,
    default: 0
    }
    },
    created() {
    this.touch = {}
    },
    methods: {
    progressTouchStart(e) {
    // 标识位, 表示已经初始化
    this.touch.initiated = true
    // 点击位置
    this.touch.startX = e.touches[0].pageX
    // 进度条宽度
    this.touch.left = this.$refs.progress.clientWidth
    },
    progressTouchMove(e) {
    if (!this.touch.initiated) {
    return
    }
    // 偏移量
    const deltaX = e.touches[0].pageX - this.touch.startX
    // 移动距离
    const offsetWidth = Math.min(this.$refs.progressBar.clientWidth - progressBtnWidth, Math.max(0, this.touch.left + deltaX))
    this._offset(offsetWidth)
    },
    progressTouchEnd() {
    this.touch.initiated = false
    this._triggerPercent()
    },
    /**
    * 点击进度条事件
    * @param {Object} e event对象
    */
    progressClick(e) {
    const rect = this.$refs.progressBar.getBoundingClientRect()
    const offsetWidth = e.pageX - rect.left
    this._offset(offsetWidth)
    // 这里当我们点击 progressBtn 的时候,e.offsetX 获取不对
    // this._offset(e.offsetX)
    this._triggerPercent()
    },
    _triggerPercent() {
    const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
    const percent = this.$refs.progress.clientWidth / barWidth
    this.$emit('percentChange', percent)
    },
    _offset(offsetWidth) {
    this.$refs.progress.style.width = `${offsetWidth}px`
    this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px,0,0)`
    }
    },
    watch: {
    percent(newPercent) {
    // 没有在拖动中
    if (newPercent >= 0 && !this.touch.initiated) {
    // 进度条宽度
    const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
    // 偏移宽度
    const offsetWidth = newPercent * barWidth
    this._offset(offsetWidth)
    }
    }
    }
    }
    </script>

    <style scoped lang="stylus" rel="stylesheet/stylus">
    @import "~common/stylus/variable"
    .progress-bar
    height: 30px
    .bar-inner
    position: relative
    top: 13px
    height: 4px
    background: rgba(0, 0, 0, 0.3)
    .progress
    position: absolute
    height: 100%
    background: $color-theme
    .progress-btn-wrapper
    position: absolute
    left: -8px
    top: -13px
    width: 30px
    height: 30px
    .progress-btn
    position: relative
    top: 7px
    left: 7px
    box-sizing: border-box
    width: 16px
    height: 16px
    border: 3px solid $color-text
    border-radius: 50%
    background: $color-theme
    </style>
  3. 组件player.vue 添加事件

    1
    2
    3
    4
    5
    6
    7
    onProgressBarChange(percent) {
    const currentTime = this.currentSong.duration * percent
    this.$refs.audio.currentTime = currentTime
    if (!this.playing) {
    this.togglePlaying()
    }
    },

播放器progress-circle 圆形进度条组件实现

添加基础组件progress-circle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<template>
<div class="progress-circle">
<!-- viewBox和r成比例 -->
<svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle class="progress-background" r="50" cx="50" cy="50" fill="transparent"/>
<circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent" :stroke-dasharray="dashArray"
:stroke-dashoffset="dashOffset"/>
</svg>
<slot></slot>
</div>
</template>

<script>
export default {
props: {
radius: {
type: Number,
default: 100
},
percent: {
type: Number,
default: 0
}
},
data() {
return {
dashArray: Math.PI * 100
}
},
computed: {
dashOffset() {
return (1 - this.percent) * this.dashArray
}
}
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
.progress-circle
position: relative
circle
stroke-width: 8px
transform-origin: center
&.progress-background
transform: scale(0.9)
stroke: $color-theme-d
&.progress-bar
transform: scale(0.9) rotate(-90deg)
stroke: $color-theme
</style>

播放器模式切换功能实现

  1. 添加common/js/util.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min)
    }

    export function shuffle(arr) {
    let _arr = arr.slice()
    for (let i = 0; i < _arr.length; i++) {
    let j = getRandomInt(0, i)
    let t = _arr[i]
    _arr[i] = _arr[j]
    _arr[j] = t
    }
    return _arr
    }
  2. 在play.vue中添加方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    changeMode() {
    const mode = (this.mode + 1) % 3
    this.setPlayMode(mode)
    let list = null
    if (mode === playMode.random) {
    list = shuffle(this.sequenceList)
    } else {
    list = this.sequenceList
    }
    this.resetCurrentIndex(list)
    this.setPlaylist(list)
    },
    resetCurrentIndex(list) {
    let index = list.findIndex((item) => {
    return item.id === this.currentSong.id
    })
    this.setCurrentIndex(index)
    },
  3. 在player.vue中添加方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    end() {
    if (this.mode === playMode.loop) {
    this.loop()
    } else {
    this.next()
    }
    },
    loop() {
    this.$refs.audio.currentTime = 0
    this.$refs.audio.play()
    },
  4. 在store/action中添加乱序播放方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    export const randomPlay = function ({commit}, {list}) {
    commit(types.SET_PLAY_MODE, playMode.random)
    commit(types.SET_SEQUENCE_LIST, list)
    let randomList = shuffle(list)
    commit(types.SET_PLAYLIST, randomList)
    commit(types.SET_CURRENT_INDEX, 0)
    commit(types.SET_FULL_SCREEN, true)
    commit(types.SET_PLAYING_STATE, true)
    }
  5. 在music-list.vue中添加方法

    1
    2
    3
    4
    5
    random() {
    this.randomPlay({
    list: this.songs
    })
    },
  6. 修改store/action

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function findIndex(list, song) {
    return list.findIndex((item) => {
    return item.id === song.id
    })
    }

    export const selectPlay = function({ commit, state }, { list, index }) {
    commit(types.SET_SEQUENCE_LIST, list)
    if (state.mode === playMode.random) {
    let randomList = shuffle(list)
    commit(types.SET_PLAYLIST, randomList)
    index = findIndex(randomList, list[index])
    } else {
    commit(types.SET_PLAYLIST, list)
    }
    commit(types.SET_CURRENT_INDEX, index)
    commit(types.SET_FULL_SCREEN, true)
    commit(types.SET_PLAYING_STATE, true)
    }

播放器歌词数据抓取

  1. 添加api/song.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import {commonParams} from './config'
    import axios from 'axios'

    export function getLyric(mid) {
    const url = '/api/lyric'

    const data = Object.assign({}, commonParams, {
    songmid: mid,
    platform: 'yqq',
    hostUin: 0,
    needNewCode: 0,
    categoryId: 10000000,
    pcachetime: +new Date(),
    format: 'json'
    })

    return axios.get(url, {
    params: data
    }).then((res) => {
    return Promise.resolve(res.data)
    })
    }
  2. build/dev-server.js添加方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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)
    })
    })
  3. song.js class 添加获取歌词方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    getLyric() {
    if (this.lyric) {
    return Promise.resolve(this.lyric)
    }

    return new Promise((resolve, reject) => {
    getLyric(this.mid).then((res) => {
    if (res.retcode === ERR_OK) {
    this.lyric = Base64.decode(res.lyric)
    resolve(this.lyric)
    } else {
    reject('no lyric')
    }
    })
    })
    }

播放器歌词滚动列表实现

1
2
3
4
5
6
7
8
9
10
11
handleLyric({lineNum, txt}) {
this.currentLineNum = lineNum
// 超过5行, 滚动到lineNum - 5, 5行之内, 滚动到顶部
if (lineNum > 5) {
let lineEl = this.$refs.lyricLine[lineNum - 5]
this.$refs.lyricList.scrollToElement(lineEl, 1000)
} else {
this.$refs.lyricList.scrollTo(0, 0, 1000)
}
this.playingLyric = txt
},

播放器歌词左右滑动实现

  1. created时添加变量touch

    1
    2
    3
    created() {
    this.touch = {}
    },
  2. 添加滑动事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    middleTouchStart(e) {
    this.touch.initiated = true
    // 用来判断是否是一次移动
    this.touch.moved = false
    const touch = e.touches[0]
    this.touch.startX = touch.pageX
    this.touch.startY = touch.pageY
    },
    middleTouchMove(e) {
    if (!this.touch.initiated) {
    return
    }
    const touch = e.touches[0]
    const deltaX = touch.pageX - this.touch.startX
    const deltaY = touch.pageY - this.touch.startY
    if (Math.abs(deltaY) > Math.abs(deltaX)) {
    return
    }
    if (!this.touch.moved) {
    this.touch.moved = true
    }
    const left = this.currentShow === 'cd' ? 0 : -window.innerWidth
    const offsetWidth = Math.min(0, Math.max(-window.innerWidth, left + deltaX))
    this.touch.percent = Math.abs(offsetWidth / window.innerWidth)
    this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)`
    this.$refs.lyricList.$el.style[transitionDuration] = 0
    this.$refs.middleL.style.opacity = 1 - this.touch.percent
    this.$refs.middleL.style[transitionDuration] = 0
    },
    middleTouchEnd() {
    if (!this.touch.moved) {
    return
    }
    let offsetWidth
    let opacity
    if (this.currentShow === 'cd') {
    // 从右向左滑, 超过10%, 滚动屏幕
    if (this.touch.percent > 0.1) {
    offsetWidth = -window.innerWidth
    opacity = 0
    this.currentShow = 'lyric'
    } else {
    offsetWidth = 0
    opacity = 1
    }
    } else {
    if (this.touch.percent < 0.9) {
    offsetWidth = 0
    this.currentShow = 'cd'
    opacity = 1
    } else {
    offsetWidth = -window.innerWidth
    opacity = 0
    }
    }
    const time = 300
    this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)`
    this.$refs.lyricList.$el.style[transitionDuration] = `${time}ms`
    this.$refs.middleL.style.opacity = opacity
    this.$refs.middleL.style[transitionDuration] = `${time}ms`
    this.touch.initiated = false
    },

播放器歌词剩余功能实现

播放器底部播放器适配+mixin的应用

添加common/js/mixin.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {mapGetters} from 'vuex'

export const playlistMixin = {
computed: {
...mapGetters([
'playlist'
])
},
mounted() {
this.handlePlaylist(this.playlist)
},
activated() {
this.handlePlaylist(this.playlist)
},
watch: {
playlist(newVal) {
this.handlePlaylist(newVal)
}
},
methods: {
handlePlaylist() {
throw new Error('component must implement handlePlaylist method')
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
// 引入mixin
import {playlistMixin} from 'common/js/mixin'

// 使用mixin
mixins: [playlistMixin],

// 添加handlePlaylist方法
handlePlaylist(playlist) {
const bottom = playlist.length > 0 ? '60px' : ''
// this.$refs.list.$el 是 this.$refs.list的DOM元素
this.$refs.list.$el.style.bottom = bottom
this.$refs.list.refresh()
},
1…5678

Taohong

记录前端技术,学习心得与生活感悟

80 posts
6 categories
65 tags
© 2020 Taohong
Powered by Hexo
|
Theme — NexT.Muse v5.1.4