如何在 React 中创建 Spotify 音乐搜索应用
介绍
在本文中,您将使用 Spotify 音乐 API 创建一个完全响应的 Spotify 音乐搜索应用程序。
通过创建此应用程序,您将学习
- 如何使用 Spotify API 提供 OAuth 身份验证
- 如何搜索专辑、艺术家和播放列表
- 通过美观的用户界面展示详细信息
- 直接从列表中播放歌曲
- 如何向应用添加加载更多功能
- 如何为专辑、艺术家和播放列表添加和维护单独的加载更多功能
等等。
您可以在下面的视频中看到最终工作应用程序的现场演示
要使用 Redux Toolkit 和 React Router 6 构建此应用程序,请查看本课程。
初始设置
使用以下方法创建新项目create-react-app
:
create-react-app spotify-music-search-app
项目创建完成后,删除文件夹中的所有文件,并在文件夹中src
创建index.js
和styles.css
文件src
。同时,在文件夹中创建actions
、、、、和文件夹。components
images
reducers
router
store
utils
src
安装必要的依赖项:
yarn add axios@0.19.2 bootstrap@4.5.2 lodash@4.17.19 prop-types@15.7.2 react-bootstrap@1.3.0 redux@4.0.5 react-redux@7.2.1 react-router-dom@5.2.0 redux-thunk@2.3.0
打开并从这里styles.css
添加内容。
创建初始页面
Header.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
const Header = () => {
return <h1 className="main-heading">Spotify Music Search</h1>;
};
export default Header;
RedirectPage.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
const RedirectPage = () => {
return <div>Redirect Page</div>;
};
export default RedirectPage;
Dashboard.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
const Dashboard = () => {
return <div>Dashboard Page</div>;
};
export default Dashboard;
Home.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
import { connect } from 'react-redux';
import { Button } from 'react-bootstrap';
import Header from './Header';
const Home = (props) => {
return (
<div className="login">
<Header />
<Button variant="info" type="submit">
Login to spotify
</Button>
</div>
);
};
export default connect()(Home);
NotFoundPage.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
import { Link } from 'react-router-dom';
import Header from './Header';
const NotFoundPage = () => {
return (
<React.Fragment>
<Header />
Page not found. Goto <Link to="/dashboard">Home Page</Link>
</React.Fragment>
);
};
export default NotFoundPage;
AppRouter.js
在文件夹内创建一个新文件router
,内容如下:
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from '../components/Home';
import RedirectPage from '../components/RedirectPage';
import Dashboard from '../components/Dashboard';
import NotFoundPage from '../components/NotFoundPage';
class AppRouter extends React.Component {
render() {
return (
<BrowserRouter>
<div className="main">
<Switch>
<Route path="/" component={Home} exact={true} />
<Route path="/redirect" component={RedirectPage} />
<Route path="/dashboard" component={Dashboard} />
<Route component={NotFoundPage} />
</Switch>
</div>
</BrowserRouter>
);
}
}
export default AppRouter;
在这里,我们使用库为主页、仪表板页面、未找到页面和重定向页面等各种页面设置了路由react-router-dom
。
albums.js
在文件夹内创建一个新文件reducers
,内容如下:
const albumsReducer = (state = {}, action) => {
switch (action.type) {
default:
return state;
}
};
export default albumsReducer;
artists.js
在文件夹内创建一个新文件reducers
,内容如下:
const artistsReducer = (state = {}, action) => {
switch (action.type) {
default:
return state;
}
};
export default artistsReducer;
playlist.js
在文件夹内创建一个新文件reducers
,内容如下:
const playlistReducer = (state = {}, action) => {
switch (action.type) {
default:
return state;
}
};
export default playlistReducer;
在所有上述 Reducer 中,我们都设置了默认状态。随着应用的进展,我们将添加更多 switch case。
store.js
在文件夹内创建一个新文件store
,内容如下:
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import albumsReducer from '../reducers/albums';
import artistsReducer from '../reducers/artists';
import playlistReducer from '../reducers/playlist';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
combineReducers({
albums: albumsReducer,
artists: artistsReducer,
playlist: playlistReducer
}),
composeEnhancers(applyMiddleware(thunk))
);
export default store;
在这里,我们创建了一个将所有 Reducer 组合在一起的 Redux 存储,以便我们可以从AppRouter.js
文件中定义的任何组件访问存储数据。
现在,打开src/index.js
文件并在其中添加以下内容:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store/store';
import AppRouter from './router/AppRouter';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles.css';
ReactDOM.render(
<Provider store={store}>
<AppRouter />
</Provider>,
document.getElementById('root')
);
在这里,我们添加了一个Provider
组件,它将把 redux 存储传递给组件中声明的所有路由AppRouter
。
现在,通过从终端运行以下命令来启动 React App:
yarn start
当您访问http://localhost:3000/ 上的应用程序时,您将看到以下屏幕
添加登录身份验证功能
现在,让我们添加登录功能。要使用 App 登录 Spotify 帐户,您需要三样东西:client_id
、authorize_url
和redirect_url
。
要获得该信息,请导航至此处并登录 Spotify 开发者帐户(如果您没有帐户,请注册)。
登录后,您将看到类似于以下屏幕的页面来创建应用程序。
单击CREATE AN APP
绿色按钮并输入应用程序名称和描述,然后单击CREATE
按钮。
记下生成的客户端 ID。
然后点击EDIT SETTINGS
按钮。输入http://localhost:3000/redirect作为 的值Redirect URIs
,然后点击按钮ADD
,然后SAVE
滚动一下鼠标点击按钮。
.env
现在,在项目根目录中创建一个名称相同的新文件,并在其中添加以下详细信息。
REACT_APP_CLIENT_ID=your_client_id
REACT_APP_AUTHORIZE_URL=https://accounts.spotify.com/authorize
REACT_APP_REDIRECT_URL=http://localhost:3000/redirect
这里,
REACT_APP_AUTHORIZE_URL
将用于显示授权弹出窗口以从您的应用访问您的 Spotify 帐户。REACT_APP_REDIRECT_URL
将是用户成功授权后重定向到的 URL。- 每个变量都以
REACT_APP_
so开头,因此Create React App
会自动在对象中添加这些变量,process.env
以使其在应用程序中可访问。
确保将
.env
文件添加到.gitignore
文件内部,这样它就不会被添加到 git,因为它包含不应公开的私人信息
请注意,变量的值必须与上面显示的屏幕截图中REACT_APP_REDIRECT_URL
输入的值相匹配,否则应用程序将无法运行。Redirect URIs
Edit settings
现在,打开src/components/Home.js
并将onClick
处理程序添加到登录按钮
<Button variant="info" type="submit" onClick={handleLogin}>
Login to spotify
</Button>
并添加handleLogin
函数
const {
REACT_APP_CLIENT_ID,
REACT_APP_AUTHORIZE_URL,
REACT_APP_REDIRECT_URL
} = process.env;
const handleLogin = () => {
window.location = `${REACT_APP_AUTHORIZE_URL}?client_id=${REACT_APP_CLIENT_ID}&redirect_uri=${REACT_APP_REDIRECT_URL}&response_type=token&show_dialog=true`;
};
更新后的Home.js
文件将如下所示:
import React from 'react';
import { connect } from 'react-redux';
import { Button } from 'react-bootstrap';
import Header from './Header';
const Home = (props) => {
const {
REACT_APP_CLIENT_ID,
REACT_APP_AUTHORIZE_URL,
REACT_APP_REDIRECT_URL
} = process.env;
const handleLogin = () => {
window.location = `${REACT_APP_AUTHORIZE_URL}?client_id=${REACT_APP_CLIENT_ID}&redirect_uri=${REACT_APP_REDIRECT_URL}&response_type=token&show_dialog=true`;
};
return (
<div className="login">
<Header />
<Button variant="info" type="submit" onClick={handleLogin}>
Login to spotify
</Button>
</div>
);
};
export default connect()(Home);
现在,通过yarn start
从终端运行命令来启动您的应用程序并验证登录功能

如您所见,一旦我们单击AGREE
按钮,我们就会被重定向到RedirectPage
组件,Spotify 会自动将access_token
、token_type
和添加expires_in
到我们的重定向 URL,如下所示
http://localhost:3000/redirect#access_token=BQA4Y-o2kMSWjpRMD5y55f0nXLgt51kl4UAEbjNip3lIpz80uWJQJPoKPyD-CG2jjIdCjhfZKwfX5X6K7sssvoe20GJhhE7bHPaW1tictiMlkdzkWe2Pw3AnmojCy-NzVSOCj-aNtQ8ztTBYrCzRiBFGPtAn-I5g35An10&token_type=Bearer&expires_in=3600
access_token
是一个 Bearer 令牌,稍后您将把它添加到对 Spotify API 发出的每个请求中。expires_in
指定令牌的过期时间,3600
默认为秒,即1小时。过期后,您需要重新登录。
添加搜索功能
现在,我们可以访问令牌,我们需要将其存储在某个地方,以便我们可以将其用于每个 API 请求。
functions.js
在文件夹中创建一个新文件,其名称src/utils
包含以下内容:
import axios from 'axios';
export const getParamValues = (url) => {
return url
.slice(1)
.split('&')
.reduce((prev, curr) => {
const [title, value] = curr.split('=');
prev[title] = value;
return prev;
}, {});
};
export const setAuthHeader = () => {
try {
const params = JSON.parse(localStorage.getItem('params'));
if (params) {
axios.defaults.headers.common[
'Authorization'
] = `Bearer ${params.access_token}`;
}
} catch (error) {
console.log('Error setting auth', error);
}
};
在这里,我们添加了,
getParamValues
函数将存储access_token
、token_type
和expires_in
值在一个对象中,如下所示:
{
access_token: some_value,
token_type: some_value,
expires_in: some_value
}
setAuthHeader
函数将添加access_token
到每个axios
API 请求中
打开RedirectPage.js
文件并将其替换为以下内容:
import React from 'react';
import _ from 'lodash';
import { getParamValues } from '../utils/functions';
export default class RedirectPage extends React.Component {
componentDidMount() {
const { setExpiryTime, history, location } = this.props;
try {
if (_.isEmpty(location.hash)) {
return history.push('/dashboard');
}
const access_token = getParamValues(location.hash);
const expiryTime = new Date().getTime() + access_token.expires_in * 1000;
localStorage.setItem('params', JSON.stringify(access_token));
localStorage.setItem('expiry_time', expiryTime);
history.push('/dashboard');
} catch (error) {
history.push('/');
}
}
render() {
return null;
}
}
在这里,我们添加了一个componentDidMount
生命周期方法来访问 URL 参数并将其存储在本地存储中。我们getParamValues
通过传递 URL 中可用的值来调用该函数location.hash
。
该expires_in
值以秒 ( &expires_in=3600
) 为单位,因此我们将其乘以1000
,然后将其添加到当前时间的毫秒数,将其转换为毫秒
const expiryTime = new Date().getTime() + access_token.expires_in * 1000;
因此expiryTime
将包含令牌生成时间后一小时的毫秒数(因为 expires_in 是 3600)。
constants.js
在文件夹中创建一个新文件utils
,内容如下:
export const SET_ALBUMS = 'SET_ALBUMS';
export const ADD_ALBUMS = 'ADD_ALBUMS';
export const SET_ARTISTS = 'SET_ARTISTS';
export const ADD_ARTISTS = 'ADD_ARTISTS';
export const SET_PLAYLIST = 'SET_PLAYLIST';
export const ADD_PLAYLIST = 'ADD_PLAYLIST';
result.js
在文件夹内创建一个新文件actions
,内容如下:
import {
SET_ALBUMS,
ADD_ALBUMS,
SET_ARTISTS,
ADD_ARTISTS,
SET_PLAYLIST,
ADD_PLAYLIST
} from '../utils/constants';
import { get } from '../utils/api';
export const setAlbums = (albums) => ({
type: SET_ALBUMS,
albums
});
export const addAlbums = (albums) => ({
type: ADD_ALBUMS,
albums
});
export const setArtists = (artists) => ({
type: SET_ARTISTS,
artists
});
export const addArtists = (artists) => ({
type: ADD_ARTISTS,
artists
});
export const setPlayList = (playlists) => ({
type: SET_PLAYLIST,
playlists
});
export const addPlaylist = (playlists) => ({
type: ADD_PLAYLIST,
playlists
});
export const initiateGetResult = (searchTerm) => {
return async (dispatch) => {
try {
const API_URL = `https://api.spotify.com/v1/search?query=${encodeURIComponent(
searchTerm
)}&type=album,playlist,artist`;
const result = await get(API_URL);
console.log(result);
const { albums, artists, playlists } = result;
dispatch(setAlbums(albums));
dispatch(setArtists(artists));
return dispatch(setPlayList(playlists));
} catch (error) {
console.log('error', error);
}
};
};
api.js
在文件夹内创建一个新文件utils
,内容如下:
import axios from 'axios';
import { setAuthHeader } from './functions';
export const get = async (url, params) => {
setAuthHeader();
const result = await axios.get(url, params);
return result.data;
};
export const post = async (url, params) => {
setAuthHeader();
const result = await axios.post(url, params);
return result.data;
};
Authorization
在这个文件中,我们使用 axios 进行 API 调用,但在此之前,我们通过调用函数在 Header中添加 access_token setAuthHeader
。
Loader.js
在文件夹内创建一个新文件components
,内容如下:
import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
const Loader = (props) => {
const [node] = useState(document.createElement('div'));
const loader = document.querySelector('#loader');
useEffect(() => {
loader.appendChild(node).classList.add('message');
}, [loader, node]);
useEffect(() => {
if (props.show) {
loader.classList.remove('hide');
document.body.classList.add('loader-open');
} else {
loader.classList.add('hide');
document.body.classList.remove('loader-open');
}
}, [loader, props.show]);
return ReactDOM.createPortal(props.children, node);
};
export default Loader;
在这个文件中,我们创建了一个加载器组件,它将使用背景覆盖层显示加载消息。我们使用ReactDOM.createPortal
方法来创建加载器。
要将加载器添加到页面,请打开public/index.html
文件并在带有 id 的 div 后添加加载器 divroot
您的index.html
页面主体现在将如下所示:
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="loader" class="hide"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
默认情况下,加载器将被隐藏,因此我们添加了该类,hide
而在显示加载器时我们将删除该类hide
。
SearchForm.js
在文件夹内创建一个新文件components
,内容如下:
import React, { useState } from 'react';
import { Form, Button } from 'react-bootstrap';
const SearchForm = (props) => {
const [searchTerm, setSearchTerm] = useState('');
const [errorMsg, setErrorMsg] = useState('');
const handleInputChange = (event) => {
const searchTerm = event.target.value;
setSearchTerm(searchTerm);
};
const handleSearch = (event) => {
event.preventDefault();
if (searchTerm.trim() !== '') {
setErrorMsg('');
props.handleSearch(searchTerm);
} else {
setErrorMsg('Please enter a search term.');
}
};
return (
<div>
<Form onSubmit={handleSearch}>
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<Form.Group controlId="formBasicEmail">
<Form.Label>Enter search term</Form.Label>
<Form.Control
type="search"
name="searchTerm"
value={searchTerm}
placeholder="Search for album, artist or playlist"
onChange={handleInputChange}
autoComplete="off"
/>
</Form.Group>
<Button variant="info" type="submit">
Search
</Button>
</Form>
</div>
);
};
export default SearchForm;
在这个文件中,我们添加了一个搜索框,并根据输入值更新组件的状态。
SearchResult.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
import _ from 'lodash';
import AlbumsList from './AlbumsList';
const SearchResult = (props) => {
const { result, setCategory, selectedCategory } = props;
const { albums, artists, playlist } = result;
return (
<React.Fragment>
<div className="search-buttons">
{!_.isEmpty(albums.items) && (
<button
className={`${
selectedCategory === 'albums' ? 'btn active' : 'btn'
}`}
onClick={() => setCategory('albums')}
>
Albums
</button>
)}
{!_.isEmpty(artists.items) && (
<button
className={`${
selectedCategory === 'artists' ? 'btn active' : 'btn'
}`}
onClick={() => setCategory('artists')}
>
Artists
</button>
)}
{!_.isEmpty(playlist.items) && (
<button
className={`${
selectedCategory === 'playlist' ? 'btn active' : 'btn'
}`}
onClick={() => setCategory('playlist')}
>
PlayLists
</button>
)}
</div>
<div className={`${selectedCategory === 'albums' ? '' : 'hide'}`}>
{albums && <AlbumsList albums={albums} />}
</div>
</React.Fragment>
);
};
export default SearchResult;
在文件夹内从此处images
添加图像并命名为music.jpeg
如果任何专辑、艺术家或播放列表的图像不存在,我们将使用此图像作为默认图像。
AlbumsList.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
import { Card } from 'react-bootstrap';
import _ from 'lodash';
import music from '../images/music.jpeg';
const AlbumsList = ({ albums }) => {
return (
<React.Fragment>
{Object.keys(albums).length > 0 && (
<div className="albums">
{albums.items.map((album, index) => {
return (
<React.Fragment key={index}>
<Card style={{ width: '18rem' }}>
<a
target="_blank"
href={album.external_urls.spotify}
rel="noopener noreferrer"
className="card-image-link"
>
{!_.isEmpty(album.images) ? (
<Card.Img
variant="top"
src={album.images[0].url}
alt=""
/>
) : (
<img src={music} alt="" />
)}
</a>
<Card.Body>
<Card.Title>{album.name}</Card.Title>
<Card.Text>
<small>
{album.artists.map((artist) => artist.name).join(', ')}
</small>
</Card.Text>
</Card.Body>
</Card>
</React.Fragment>
);
})}
</div>
)}
</React.Fragment>
);
};
export default AlbumsList;
yarn start
现在,通过运行命令启动应用程序
如您所见,当我们搜索任何内容时,Spotify API 的响应都会显示在控制台中。因此,我们能够成功从 Spotify 访问音乐数据。
在用户界面上显示专辑
现在,我们将在 redux 存储中添加响应,以便我们可以在 UI 上显示它。
打开src/reducers/albums.js
文件并将其替换为以下内容:
import { SET_ALBUMS, ADD_ALBUMS } from '../utils/constants';
const albumsReducer = (state = {}, action) => {
const { albums } = action;
switch (action.type) {
case SET_ALBUMS:
return albums;
case ADD_ALBUMS:
return {
...state,
next: albums.next,
items: [...state.items, ...albums.items]
};
default:
return state;
}
};
export default albumsReducer;
现在,yarn start
再次运行该命令并检查应用程序
如您所见,当我们搜索时,redux store 会更新,并将结果显示在 UI 上。让我们了解一下此功能的代码。
在Dashboard.js
文件中,我们调用用户点击搜索按钮时触发的initiateGetResult
内部函数。handleSearch
如果您检查文件initiateGetResult
中的函数,我们会通过将搜索文本作为查询参数传递actions/result.js
到 URL 来发出 API 调用https://api.spotify.com/v1/search
export const initiateGetResult = (searchTerm) => {
return async (dispatch) => {
try {
const API_URL = `https://api.spotify.com/v1/search?query=${encodeURIComponent(
searchTerm
)}&type=album,playlist,artist`;
const result = await get(API_URL);
console.log(result);
const { albums, artists, playlists } = result;
dispatch(setAlbums(albums));
dispatch(setArtists(artists));
return dispatch(setPlayList(playlists));
} catch (error) {
console.log('error', error);
}
};
};
一旦我们得到结果,我们就会setAlbums
通过从结果中获取专辑来调用动作生成器函数。
dispatch(setAlbums(albums));
该setAlbums
函数如下所示:
export const setAlbums = (albums) => ({
type: SET_ALBUMS,
albums
});
在这里,我们返回类型为 的操作SET_ALBUMS
。因此,一旦操作被调度,albumsReducer
fromreducers/albums.js
文件就会被调用,对于匹配的SET_ALBUMS
switch 案例,我们会从 reducer 返回传递的专辑,以便 redux 存储将使用专辑数据进行更新。
case SET_ALBUMS:
return albums;
由于我们已经使用方法将Dashboard
组件(Dashboard.js
)连接到 redux 存储connect
,因此组件使用该mapStateToProps
方法获取更新的 redux 存储数据,并将结果传递给SearchResult
组件
const { albums, artists, playlist } = props;
const result = { albums, artists, playlist };
<SearchResult
result={result}
setCategory={setCategory}
selectedCategory={selectedCategory}
/>
从SearchResult
组件中,数据作为 prop 传递给AlbumsList
组件
<div className={`${selectedCategory === 'albums' ? '' : 'hide'}`}>
{albums && <AlbumsList albums={albums} />}
</div>
在组件内部AlbumsList
,我们使用 Array 方法遍历每张专辑map
并在 UI 上显示数据。
在用户界面上显示艺术家和播放列表
ArtistsList.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
import { Card } from 'react-bootstrap';
import _ from 'lodash';
import music from '../images/music.jpeg';
const ArtistsList = ({ artists }) => {
return (
<React.Fragment>
{Object.keys(artists).length > 0 && (
<div className="artists">
{artists.items.map((artist, index) => {
return (
<React.Fragment key={index}>
<Card style={{ width: '18rem' }}>
<a
target="_blank"
href={artist.external_urls.spotify}
rel="noopener noreferrer"
className="card-image-link"
>
{!_.isEmpty(artist.images) ? (
<Card.Img
variant="top"
src={artist.images[0].url}
alt=""
/>
) : (
<img src={music} alt="" />
)}
</a>
<Card.Body>
<Card.Title>{artist.name}</Card.Title>
</Card.Body>
</Card>
</React.Fragment>
);
})}
</div>
)}
</React.Fragment>
);
};
export default ArtistsList;
PlayList.js
在文件夹内创建一个新文件components
,内容如下:
import React from 'react';
import { Card } from 'react-bootstrap';
import _ from 'lodash';
import music from '../images/music.jpeg';
const PlayList = ({ playlist }) => {
return (
<div>
{Object.keys(playlist).length > 0 && (
<div className="playlist">
{playlist.items.map((item, index) => {
return (
<React.Fragment key={index}>
<Card style={{ width: '18rem' }}>
<a
target="_blank"
href={item.external_urls.spotify}
rel="noopener noreferrer"
className="card-image-link"
>
{!_.isEmpty(item.images) ? (
<Card.Img variant="top" src={item.images[0].url} alt="" />
) : (
<img src={music} alt="" />
)}
</a>
<Card.Body>
<Card.Title>{item.name}</Card.Title>
<Card.Text>
<small>By {item.owner.display_name}</small>
</Card.Text>
</Card.Body>
</Card>
</React.Fragment>
);
})}
</div>
)}
</div>
);
};
export default PlayList;
现在,打开SearchResult.js
文件并在旁边AlbumsList
添加ArtistsList
和PlayList
组件
<div className={`${selectedCategory === 'albums' ? '' : 'hide'}`}>
{albums && <AlbumsList albums={albums} />}
</div>
<div className={`${selectedCategory === 'artists' ? '' : 'hide'}`}>
{artists && <ArtistsList artists={artists} />}
</div>
<div className={`${selectedCategory === 'playlist' ? '' : 'hide'}`}>
{playlist && <PlayList playlist={playlist} />}
</div>
另外,导入文件顶部的组件
import ArtistsList from './ArtistsList';
import PlayList from './PlayList';
打开src/reducers/artists.js
文件并将其替换为以下内容:
import { SET_ARTISTS, ADD_ARTISTS } from '../utils/constants';
const artistsReducer = (state = {}, action) => {
const { artists } = action;
switch (action.type) {
case SET_ARTISTS:
return artists;
case ADD_ARTISTS:
return {
...state,
next: artists.next,
items: [...state.items, ...artists.items]
};
default:
return state;
}
};
export default artistsReducer;
打开src/reducers/playlist.js
文件并将其替换为以下内容:
import { SET_PLAYLIST, ADD_PLAYLIST } from '../utils/constants';
const playlistReducer = (state = {}, action) => {
const { playlists } = action;
switch (action.type) {
case SET_PLAYLIST:
return playlists;
case ADD_PLAYLIST:
return {
...state,
next: playlists.next,
items: [...state.items, ...playlists.items]
};
default:
return state;
}
};
export default playlistReducer;
现在,yarn start
再次运行该命令并检查应用程序
如您所见,艺术家和播放列表也填充了数据。

此外,如果您单击任意图像,您可以播放如上所示的专辑、艺术家或播放列表中的音乐。
添加加载更多功能
现在,让我们添加一个加载更多按钮来加载更多专辑、艺术家和播放列表的数据。
打开文件并在结束标签SearchResult.js
之前添加加载更多按钮</React.Fragment>
{!_.isEmpty(result[selectedCategory]) &&
!_.isEmpty(result[selectedCategory].next) && (
<div className="load-more" onClick={() => loadMore(selectedCategory)}>
<Button variant="info" type="button">
Load More
</Button>
</div>
)}
从 props 中解构loadMore
函数并Button
导入react-bootstrap
import { Button } from 'react-bootstrap';
const SearchResult = (props) => {
const { loadMore, result, setCategory, selectedCategory } = props;
打开Dashboard.js
文件并添加loadMore
函数
const loadMore = async (type) => {
const { dispatch, albums, artists, playlist } = props;
setIsLoading(true);
switch (type) {
case 'albums':
await dispatch(initiateLoadMoreAlbums(albums.next));
break;
case 'artists':
await dispatch(initiateLoadMoreArtists(artists.next));
break;
case 'playlist':
await dispatch(initiateLoadMorePlaylist(playlist.next));
break;
default:
}
setIsLoading(false);
};
并将loadMore
函数作为 prop 传递给SearchResult
组件
return (
<React.Fragment>
<Header />
<SearchForm handleSearch={handleSearch} />
<Loader show={isLoading}>Loading...</Loader>
<SearchResult
result={result}
loadMore={loadMore}
setCategory={setCategory}
selectedCategory={selectedCategory}
/>
</React.Fragment>
);
打开actions/result.js
文件并在文件末尾添加以下函数
export const initiateLoadMoreAlbums = (url) => {
return async (dispatch) => {
try {
console.log('url', url);
const result = await get(url);
console.log('categoriess', result);
return dispatch(addAlbums(result.albums));
} catch (error) {
console.log('error', error);
}
};
};
export const initiateLoadMoreArtists = (url) => {
return async (dispatch) => {
try {
console.log('url', url);
const result = await get(url);
console.log('categoriess', result);
return dispatch(addArtists(result.artists));
} catch (error) {
console.log('error', error);
}
};
};
export const initiateLoadMorePlaylist = (url) => {
return async (dispatch) => {
try {
console.log('url', url);
const result = await get(url);
console.log('categoriess', result);
return dispatch(addPlaylist(result.playlists));
} catch (error) {
console.log('error', error);
}
};
};
Dashboard.js
并在顶部的文件中导入这些函数
import {
initiateGetResult,
initiateLoadMoreAlbums,
initiateLoadMorePlaylist,
initiateLoadMoreArtists
} from '../actions/result';
现在,运行yarn start
命令并检查加载更多功能
您可以在此分支中找到截至此处的代码
会话超时时重定向到登录页面
现在,我们已经完成了应用程序的功能。让我们添加代码,以便在访问令牌过期时自动重定向到登录页面并显示会话已过期的消息。这是因为,如果会话已过期,则 API 调用将失败,但用户直到打开 devtool 控制台查看错误信息时才会发现。
如果你还记得的话RedirectPage.js
,我们在文件中添加了expiry_time
以下代码到本地存储中
const expiryTime = new Date().getTime() + access_token.expires_in * 1000;
localStorage.setItem('expiry_time', expiryTime);
现在,让我们用它来识别何时重定向到登录页面。
打开AppRouter.js
文件并将其替换为以下内容:
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from '../components/Home';
import RedirectPage from '../components/RedirectPage';
import Dashboard from '../components/Dashboard';
import NotFoundPage from '../components/NotFoundPage';
class AppRouter extends React.Component {
state = {
expiryTime: '0'
};
componentDidMount() {
let expiryTime;
try {
expiryTime = JSON.parse(localStorage.getItem('expiry_time'));
} catch (error) {
expiryTime = '0';
}
this.setState({ expiryTime });
}
setExpiryTime = (expiryTime) => {
this.setState({ expiryTime });
};
isValidSession = () => {
const currentTime = new Date().getTime();
const expiryTime = this.state.expiryTime;
const isSessionValid = currentTime < expiryTime;
return isSessionValid;
};
render() {
return (
<BrowserRouter>
<div className="main">
<Switch>
<Route path="/" component={Home} exact={true} />
<Route path="/redirect" component={RedirectPage} />
<Route path="/dashboard" component={Dashboard} />
<Route component={NotFoundPage} />
</Switch>
</div>
</BrowserRouter>
);
}
}
export default AppRouter;
在这个文件中,我们添加了一个expiryTime
默认初始化的状态变量0
,并且在componentDidMount
方法中,我们expiry_time
从本地存储中读取值并将其分配给状态。
我们还添加了setExpiryTime
和isValidSession
函数,以便我们可以在其他组件中使用它。
现在,打开RedirectPage.js
文件并在调用之前history.push('/dashboard');
添加以下代码行
setExpiryTime(expiryTime);
但是要调用这个函数,我们需要将其作为 prop 传递给RedirectPage
组件。
如果你检查组件的渲染方法AppRouter
,它看起来像这样:
render() {
return (
<BrowserRouter>
<div className="main">
<Switch>
<Route path="/" component={Home} exact={true} />
<Route path="/redirect" component={RedirectPage} />
<Route path="/dashboard" component={Dashboard} />
<Route component={NotFoundPage} />
</Switch>
</div>
</BrowserRouter>
);
}
因此,要将setExpiryTime
函数作为 prop 传递给RedirectPage
组件,我们需要将其转换为渲染 prop 模式。
因此,将下面的代码行
<Route path="/redirect" component={RedirectPage} />
到此代码:
<Route
path="/redirect"
render={(props) => (
<RedirectPage
isValidSession={this.isValidSession}
setExpiryTime={this.setExpiryTime}
{...props}
/>
)}
/>
在这里,我们将setExpiryTime
,isValidSession
函数作为 prop 传递,同时还将自动传递给 Route 的 props 分散开来,比如location
, history
。
现在,打开Dashboard.js
文件并解构道具并将handleSearch
功能更改为:
const { isValidSession, history } = props;
const handleSearch = (searchTerm) => {
if (isValidSession()) {
setIsLoading(true);
props.dispatch(initiateGetResult(searchTerm)).then(() => {
setIsLoading(false);
setSelectedCategory('albums');
});
} else {
history.push({
pathname: '/',
state: {
session_expired: true
}
});
}
};
另外,将loadMore
函数更改为:
const loadMore = async (type) => {
if (isValidSession()) {
const { dispatch, albums, artists, playlist } = props;
setIsLoading(true);
switch (type) {
case 'albums':
await dispatch(initiateLoadMoreAlbums(albums.next));
break;
case 'artists':
await dispatch(initiateLoadMoreArtists(artists.next));
break;
case 'playlist':
await dispatch(initiateLoadMorePlaylist(playlist.next));
break;
default:
}
setIsLoading(false);
} else {
history.push({
pathname: '/',
state: {
session_expired: true
}
});
}
};
将组件返回的 JSX 更改Dashboard
为:
return (
<React.Fragment>
{isValidSession() ? (
<div>
<Header />
<SearchForm handleSearch={handleSearch} />
<Loader show={isLoading}>Loading...</Loader>
<SearchResult
result={result}
loadMore={loadMore}
setCategory={setCategory}
selectedCategory={selectedCategory}
isValidSession={isValidSession}
/>
</div>
) : (
<Redirect
to={{
pathname: '/',
state: {
session_expired: true
}
}}
/>
)}
</React.Fragment>
);
另外,Redirect
在顶部导入组件:
import { Redirect } from 'react-router-dom';
打开SearchResult.js
文件并在返回 JSX 之前添加以下代码:
if (!isValidSession()) {
return (
<Redirect
to={{
pathname: '/',
state: {
session_expired: true
}
}}
/>
);
}
另外,解构isValidSession
来自的道具并Redirect
从中添加组件react-router-dom
。
现在,打开该Home.js
文件并将其替换为以下内容:
import React from 'react';
import { Alert } from 'react-bootstrap';
import { connect } from 'react-redux';
import { Button } from 'react-bootstrap';
import Header from './Header';
import { Redirect } from 'react-router-dom';
const Home = (props) => {
const {
REACT_APP_CLIENT_ID,
REACT_APP_AUTHORIZE_URL,
REACT_APP_REDIRECT_URL
} = process.env;
const handleLogin = () => {
window.location = `${REACT_APP_AUTHORIZE_URL}?client_id=${REACT_APP_CLIENT_ID}&redirect_uri=${REACT_APP_REDIRECT_URL}&response_type=token&show_dialog=true`;
};
const { isValidSession, location } = props;
const { state } = location;
const sessionExpired = state && state.session_expired;
return (
<React.Fragment>
{isValidSession() ? (
<Redirect to="/dashboard" />
) : (
<div className="login">
<Header />
{sessionExpired && (
<Alert variant="info">Session expired. Please login again.</Alert>
)}
<Button variant="info" type="submit" onClick={handleLogin}>
Login to spotify
</Button>
</div>
)}
</React.Fragment>
);
};
export default connect()(Home);
这里,我们编写了代码,/dashboard
如果会话有效则重定向到页面,否则重定向到登录页面。同时显示会话已过期的消息,以便用户了解页面重定向到登录页面的原因。
{sessionExpired && (
<Alert variant="info">Session expired. Please login again.</Alert>
)}
现在,打开AppRouter.js
文件并将isValidSession
函数传递给Home
和Dashboard
路由。
render() {
return (
<BrowserRouter>
<div className="main">
<Switch>
<Route
path="/"
exact={true}
render={(props) => (
<Home isValidSession={this.isValidSession} {...props} />
)}
/>
<Route
path="/redirect"
render={(props) => (
<RedirectPage
isValidSession={this.isValidSession}
setExpiryTime={this.setExpiryTime}
{...props}
/>
)}
/>
<Route
path="/dashboard"
render={(props) => (
<Dashboard isValidSession={this.isValidSession} {...props} />
)}
/>
<Route component={NotFoundPage} />
</Switch>
</div>
</BrowserRouter>
);
}
会话超时后,您将看到以下屏幕。
您可以在此分支中找到截至此处的代码。
结论
现在,您已完成使用 React 创建 Spotify 音乐搜索应用。您可以在此处找到此应用的完整源代码。
不要忘记订阅我的每周新闻通讯,其中包含精彩的提示、技巧和文章,直接发送到您的收件箱中。
文章来源:https://dev.to/myogeshchavan97/how-to-create-a-spotify-music-search-app-in-react-328m