這一節開發搜索功能,搜索功能將使用QQ音樂的搜索接口,獲取搜索結果數據而後利用前幾節使用的歌手、專輯、獲取歌曲文件地址接口作跳轉或者播放處理react
1.熱搜git
使用chrome瀏覽器打開手機調試模式,輸入QQ音樂手機端網址:m.y.qq.com,進入後點擊熱搜,而後點擊Network,紅色方框中就是熱搜發的請求github
點擊請求連接,選擇Preview查看返回的數據內容,其中hotkey中就是全部熱搜的關鍵詞chrome
2.搜索json
在頁面搜索輸入框中輸入搜索的內容,按回車鍵,紅色方框中就是搜索的請求redux
點開請求的連接,在Preview中查看返回的數據內容c#
其中song就是搜索結果相關的歌曲,zhida就是搜索結果相關的歌手、歌單或專輯,具體區分看裏面的type字段值api
在api目錄下面的config.js中加入接口url配置bash
config.js
const URL = {
...
/*熱搜*/
hotkey: "https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg",
/*搜索*/
search: "https://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp"
};
複製代碼
在api下面新建search.js,編寫接口請求方法
search.js
import jsonp from "./jsonp"
import {URL, PARAM, OPTION} from "./config"
export function getHotKey() {
const data = Object.assign({}, PARAM, {
g_tk: 5381,
uin: 0,
platform: "h5",
needNewCode: 1,
notice: 0,
_: new Date().getTime()
});
return jsonp(URL.hotkey, data, OPTION);
}
export function search(w) {
const data = Object.assign({}, PARAM, {
g_tk: 5381,
uin: 0,
platform: "h5",
needNewCode: 1,
notice: 0,
zhidaqu: 1,
catZhida: 1,
t: 0,
flag: 1,
ie: "utf-8",
sem: 1,
aggr: 0,
perpage: 20,
n: 20,
p: 1,
w,
remoteplace: "txt.mqq.all",
_: new Date().getTime()
});
return jsonp(URL.search, data, OPTION);
}
複製代碼
接下來根據搜索返回結果建立專輯和歌手對象函數,在model目錄下面的album.js和singer.js中分別編寫如下兩個方法
album.js
export function createAlbumBySearch(data) {
return new Album(
data.albumid,
data.albummid,
data.albumname,
`http://y.gtimg.cn/music/photo_new/T002R68x68M000${data.albummid}.jpg?max_age=2592000`,
data.singername,
""
);
}
複製代碼
singer.js
export function createSingerBySearch(data) {
return new Singer(
data.singerid,
data.singermid,
data.singername,
`http://y.gtimg.cn/music/photo_new/T001R68x68M000${data.singermid}.jpg?max_age=2592000`
);
}
複製代碼
先爲Search組件編寫容器組件Search,以便操做狀態管理中的數據。在container目錄下新建Search.js,代碼以下
import {connect} from "react-redux"
import {showPlayer, changeSong, setSongs} from "../redux/actions"
import Search from "../components/search/Search"
const mapDispatchToProps = (dispatch) => ({
showMusicPlayer: (show) => {
dispatch(showPlayer(show));
},
changeCurrentSong: (song) => {
dispatch(changeSong(song));
},
setSongs: (songs) => {
dispatch(setSongs(songs));
}
});
export default connect(null, mapDispatchToProps)(Search)
複製代碼
在App.js中將Search組件修改成容器組件
//import Search from "./search/Search"
import Search from "../containers/Search"
複製代碼
回到components下search中的Search.js。在search組件的constructor中定義如下幾個state
constructor(props) {
super(props);
this.state = {
hotKeys: [],
singer: {},
album: {},
songs: [],
w: "",
loading: false
};
}
複製代碼
hotKeys存放熱搜接口的關鍵字列表,singer存放歌手對象數據,album存放專輯對象數據,songs存放歌手列表,w對應搜索輸入框中的內容
導入Loading和Scroll組件
import Scroll from "@/common/scroll/Scroll"
import Loading from "@/common/loading/Loading"
複製代碼
render方法代碼以下
let album = this.state.album;
let singer = this.state.singer;
return (
<div className="music-search">
<div className="search-box-wrapper">
<div className="search-box">
<i className="icon-search"></i>
<input type="text" className="search-input" placeholder="搜索歌曲、歌手、專輯"
value={this.state.w}/>
</div>
<div className="cancel-button" style={{display: this.state.w ? "block" : "none"}}>取消</div>
</div>
<div className="search-hot" style={{display: this.state.w ? "none" : "block"}}>
<h1 className="title">熱門搜索</h1>
<div className="hot-list">
{
this.state.hotKeys.map((hot, index) => {
if (index > 10) return "";
return (
<div className="hot-item" key={index}>{hot.k}</div>
);
})
}
</div>
</div>
<div className="search-result skin-search-result" style={{display: this.state.w ? "block" : "none"}}>
<Scroll ref="scroll">
<div>
{/*專輯*/}
<div className="album-wrapper" style={{display:album.id ? "block" : "none"}}>
...
<div className="right">
<div className="song">{album.name}</div>
<div className="singer">{album.singer}</div>
</div>
</div>
{/*歌手*/}
<div className="singer-wrapper" style={{display:singer.id ? "block" : "none"}}>
...
<div className="right">
<div className="singer">{singer.name}</div>
<div className="info">單曲{singer.songnum} 專輯{singer.albumnum}</div>
</div>
</div>
{/*歌曲列表*/}
{
this.state.songs.map((song) => {
return (
<div className="song-wrapper" key={song.id}>
...
<div className="right">
<div className="song">{song.name}</div>
<div className="singer">{song.singer}</div>
</div>
</div>
);
})
}
</div>
<Loading title="正在加載..." show={this.state.loading}/>
</Scroll>
</div>
</div>
);
複製代碼
完整代碼和search.styl請在源碼中查看
在組件掛載完成後調用獲取熱搜關鍵詞的方法,先導入兩個以前寫好的接口的方法、接口CODE碼常量、歌手、專輯和歌曲模型類
import {getHotKey, search} from "@/api/search"
import {CODE_SUCCESS} from "@/api/config"
import * as SingerModel from "@/model/singer"
import * as AlbumModel from "@/model/album"
import * as SongModel from "@/model/song"
複製代碼
componentDidMount方法代碼以下
componentDidMount() {
getHotKey().then((res) => {
console.log("獲取熱搜:");
if (res) {
console.log(res);
if (res.code === CODE_SUCCESS) {
this.setState({
hotKeys: res.data.hotkey
});
}
}
});
}
複製代碼
編寫一個獲取搜索結果的方法,傳入搜索關鍵字作爲參數
search = (w) => {
this.setState({w, loading: true});
search(w).then((res) => {
console.log("搜索:");
if (res) {
console.log(res);
if (res.code === CODE_SUCCESS) {
let zhida = res.data.zhida;
let type = zhida.type;
let singer = {};
let album = {};
switch (type) {
//0:表示歌曲
case 0:
break;
//2:表示歌手
case 2:
singer = SingerModel.createSingerBySearch(zhida);
singer.songnum = zhida.songnum;
singer.albumnum = zhida.albumnum;
break;
//3: 表示專輯
case 3:
album = AlbumModel.createAlbumBySearch(zhida);
break;
default:
break;
}
let songs = [];
res.data.song.list.forEach((data) => {
if (data.pay.payplay === 1) { return }
songs.push(SongModel.createSong(data));
});
this.setState({
album: album,
singer: singer,
songs: songs,
loading: false
}, () => {
this.refs.scroll.refresh();
});
}
}
});
}
複製代碼
在上述代碼中,搜索接口返回的type字段分別有不一樣的值,當值爲0時,不作處理。當值爲2時建立歌手對象。當值爲3時建立專輯對象,最後調用setState方法修改state觸發組件更新。在發送請求前將輸入框的值更新爲傳遞過來的參數w,同時顯示Loading組件
在React中,可變的狀態一般保存在組件的狀態屬性中,而且只能用 setState()方法進行更新,這裏對於表單元素輸入框要把它寫成「受控組件」形式,受控組件就是React負責渲染表單的組件而後控制用戶後續輸入時所發生的變化。相應的,其值由React控制的輸入表單元素,作法就是給input輸入框添加onChange事件,值發生變化是調用setState更新
編寫一個處理change事件的方法,改變輸入框對於的we狀態屬性值,這裏要把singer、album和songs置空不然輸入內容的時候上一次搜索的結果會顯示
handleInput = (e) => {
let w = e.currentTarget.value;
this.setState({
w,
singer: {},
album: {},
songs: []
});
}
複製代碼
給input綁定change事件
<input type="text" className="search-input" placeholder="搜索歌曲、歌手、專輯"
value={this.state.w}
onChange={this.handleInput}/>
複製代碼
給取消按鈕添加點擊事件,將全部狀態屬性置空
<div className="cancel-button" style={{display: this.state.w ? "block" : "none"}}
onClick={() => this.setState({w:"", singer:{}, album:{}, songs:[]})}>取消</div>
複製代碼
當點擊熱搜關鍵詞時,調用search方法,而且傳入當前點擊的關鍵字調用搜索接口進行搜索
handleSearch = (k) => {
return () => {
this.search(k);
}
}
複製代碼
<div className="hot-item" key={index}
onClick={this.handleSearch(hot.k)}>{hot.k}</div>
複製代碼
接下來處理搜索結果內容的點擊,搜索結果分兩個方式展現,以下兩個圖
第一張圖最上面是搜索結果中的歌手信息,第二張圖最上面是搜索結果中的專輯信息,兩張圖最下面是搜索的歌曲列表。點擊歌手跳轉到歌手詳情,點擊專輯跳轉到專輯詳情,這裏使用以前寫好的Singer和Album組件
給Search組件增長歌手和專輯兩個子路由,導入Route、Singer和Album容器組件
import {Route} from "react-router-dom"
import Album from "@/containers/Album"
import Singer from "@/containers/Singer"
複製代碼
放置在以下位置
<div className="music-search">
...
<Route path={`${this.props.match.url + '/album/:id'}`} component={Album} />
<Route path={`${this.props.match.url + '/singer/:id'}`} component={Singer} />
</div>
複製代碼
給.album-wrapper和.singer-wrapper元素添加點擊事件
<div className="album-wrapper" style={{display:album.id ? "block" : "none"}}
onClick={this.handleClick(album.mId, "album")}>
...
</div>
<div className="singer-wrapper" style={{display:singer.id ? "block" : "none"}}
onClick={this.handleClick(singer.mId, "singer")}>
...
</div>
複製代碼
handleClick = (data, type) => {
return (e) => {
switch (type) {
case "album":
//跳轉到專輯詳情
this.props.history.push({
pathname: `${this.props.match.url}/album/${data}`
});
break;
case "singer":
//跳轉到歌手詳情
this.props.history.push({
pathname: `${this.props.match.url}/singer/${data}`
});
break;
case "song":
break;
default:
break;
}
}
}
複製代碼
上訴代碼點擊專輯或歌手跳轉到相應的路由組件
繼續處理歌曲點擊,導入獲取歌曲vkey函數
import {getSongVKey} from "@/api/song"
複製代碼
handleClick = (data, type) => {
return (e) => {
...
case "song":
getSongVKey(data.mId).then((res) => {
if (res) {
if(res.code === CODE_SUCCESS) {
if(res.data.items) {
let item = res.data.items[0];
data.url = `http://dl.stream.qqmusic.qq.com/${item.filename}?vkey=${item.vkey}&guid=3655047200&fromtag=66`;
this.props.setSongs([data]);
this.props.changeCurrentSong(data);
}
}
}
});
break;
...
}
}
複製代碼
給.song-wrapper元素綁定點擊事件
<div className="song-wrapper" key={song.id} onClick={this.handleClick(song, "song")}>
...
</div>
複製代碼
點擊歌曲修改Redux中的歌曲和歌曲列表,觸發Play組件播放點擊的歌曲
複製第5節的initMusicIco和startMusicIcoAnimation兩個函數,而後在componentDidMount中調用initMusicIco
import ReactDOM from "react-dom"
import {getTransitionEndName} from "@/util/event"
複製代碼
this.initMusicIco();
複製代碼
在handleClick函數中當type參數等於song時調用startMusicIcoAnimation啓動動畫
handleClick = (data, type) => {
return (e) => {
...
case "song":
this.startMusicIcoAnimation(e.nativeEvent);
getSongVKey(data.mId).then((res) => {
...
});
break;
...
}
}
複製代碼
音符下落動畫具體請看歌曲點擊音符下落動畫
通過運行點擊歌曲發現出現如下異常
這是由於Play.js中寫了如下代碼,這裏原本是兼容手機端有些瀏覽器第一次沒法播放的問題,後來測試發現如下代碼不存在第一次也能夠自動播放。由於React全部的事件都統一由document代理,而後分發到具體綁定事件的對象上去,document接收點擊事件後瀏覽器便知道用戶觸摸了屏幕,此時調用play()方法就能夠正常進行播放
componentDidUpdate() {
//兼容手機端canplay事件觸發後第一次調用play()方法沒法自動播放的問題
if (this.isFirstPlay === true) {
this.audioDOM.play();
this.isFirstPlay = false;
}
}
複製代碼
Play組件中的以上代碼已在這一節中刪除
這一節開發了搜索頁面,主要利用搜索接口,在搜索不一樣的結果作不一樣的處理。搜索結果頁面使用了前幾節已經開發好的頁面
完整項目地址:github.com/code-mcx/ma…
本章節代碼在chapter8分支
後續更新中...