React全家桶構建一款Web音樂App實戰(八):搜尋功能開發

code_mcx發表於2018-01-29

這一節開發搜尋功能,搜尋功能將使用QQ音樂的搜尋介面,獲取搜尋結果資料然後利用前幾節使用的歌手、專輯、獲取歌曲檔案地址介面做跳轉或者播放處理

介面資料抓取

1.熱搜

使用chrome瀏覽器開啟手機除錯模式,輸入QQ音樂手機端網址:m.y.qq.com,進入後點選熱搜,然後點選Network,紅色方框中就是熱搜發的請求

React全家桶構建一款Web音樂App實戰(八):搜尋功能開發

點選請求連結,選擇Preview檢視返回的資料內容,其中hotkey中就是所有熱搜的關鍵詞

React全家桶構建一款Web音樂App實戰(八):搜尋功能開發

2.搜尋

在頁面搜尋輸入框中輸入搜尋的內容,按Enter鍵,紅色方框中就是搜尋的請求

React全家桶構建一款Web音樂App實戰(八):搜尋功能開發

點開請求的連結,在Preview中檢視返回的資料內容

React全家桶構建一款Web音樂App實戰(八):搜尋功能開發

其中song就是搜尋結果相關的歌曲,zhida就是搜尋結果相關的歌手、歌單或專輯,具體區分看裡面的type欄位值

介面具體說明請看QQ音樂api梳理熱搜搜尋介面

介面請求方法

在api目錄下面的config.js中加入介面url配置

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.jssinger.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>
複製程式碼

搜尋結果處理

接下來處理搜尋結果內容的點選,搜尋結果分兩個方式展示,如下兩個圖

React全家桶構建一款Web音樂App實戰(八):搜尋功能開發
React全家桶構建一款Web音樂App實戰(八):搜尋功能開發

第一張圖最上面是搜尋結果中的歌手資訊,第二張圖最上面是搜尋結果中的專輯資訊,兩張圖最下面是搜尋的歌曲列表。點選歌手跳轉到歌手詳情,點選專輯跳轉到專輯詳情,這裡使用之前寫好的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節的initMusicIcostartMusicIcoAnimation兩個函式,然後在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;
        ... 
    }
}
複製程式碼

音符下落動畫具體請看歌曲點選音符下落動畫

經過執行點選歌曲發現出現以下異常

React全家桶構建一款Web音樂App實戰(八):搜尋功能開發

這是因為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分支

後續更新中...

相關文章