组件构成
- App.js — 其他 3 个组件的父组件:包含处理 API 请求的函数,包含组件初始呈现期间调用 API 的函数。
- Header.js — 呈现应用标题并接受标题属性。
- Movie.js — 渲染每部电影。
- Search.js — 包含一个带有 input 元素和搜索按钮的表单,包含处理 input 元素和重置字段的函数,还包含一个调用 search 函数的函数,该函数作为 props 传递给它。
步骤
1.设置React应用程序
npx create-react-app movie-search
2.设置组件
1)在src下创建一个文件夹components
包含“组件构成”中所述四个组件。
2)在index.js中更新导入
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App'; // this changed
ReactDOM.render(<App />, document.getElementById('root'));
3)设置Header.js并导出
在Header.js中添加如下代码,创建一个用text属性渲染header标签的组件。
import React from "react";
const Header = (props) => {
return (
<header className="App-header">
<h2>{props.text}</h2>
</header>
);
};
export default Header;
4)更新App.css
以下代码包含项目全部样式
.App {
text-align: center;
}
/*标题样式 */
.App-header {
/* flex布局 */
background-color: #282c34;
height: 70px;
display: flex;
flex-direction: column;
justify-self: center;
align-items: center;
/* vmin:当前视窗宽度和高度的百分比中较小的那个,1vw 代表视窗的宽度为 1%
使文字大小在横竖屏时保持一致*/
font-size: calc(10px + 2vmin);
color: white;
padding: 20px;
cursor: pointer;
}
.loading {
display: block;
width: 20%;
margin: auto;
}
.App-intro {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-size: calc(10px + 2vmin);
}
/* new css for movie component */
* {
/* width 和 height 属性包括内容,内边距和边框,但不包括外边距 */
box-sizing: border-box;
}
.movies {
display: flex;
flex-wrap: wrap;
flex-direction: row;
}
.movies h2 {
display: inline-block;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
height: 45px;
width: 200px;
font-size: medium;
}
.App-header h2 {
margin: 0;
}
.movie {
padding: 5px 25px 10px 25px;
/* max-width 会覆盖width设置, 但 min-width设置会覆盖 max-width */
/* 以父级块级容器宽度的百分比<percentage>作为最大宽度. */
max-width: 25%;
}
.errorMessage {
margin: auto;
font-weight: bold;
color: rgb(161, 15, 15);
}
.search {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
margin-top: 10px;
}
.input[type="submit"] {
padding: 5px;
background-color: transparent;
color: black;
border: 1px solid black;
width: 80px;
margin-left: 5px;
cursor: pointer;
}
.input[type="submit"]:hover {
background-color: #282c34;
color: lightskyblue;
}
.search > input[type="text"] {
width: 40%;
min-width: 170px;
}
@media screen and (min-width: 694px) and (max-width: 915px) {
.movie {
max-width: 33%;
}
}
@media screen and (min-width: 652px) and (max-width: 693px) {
.movie {
max-width: 50%;
}
}
@media screen and (max-width: 651px) {
.movie {
max-width: 100%;
margin: auto;
}
}
5) 设置Movie.js组件并导出
呈现电影标题、图像和年份的展示组件,没有任何内部状态。
设置DEFAULT_PLACEHOLDER_IMAGE以替从API检索到的、没有图像的电影,提供占位符图像。
import React from "react";
const DEFAULT_PLACEHOLDER_IMAGE =
"https://m.media-amazon.com/images/M/MV5BMTczNTI2ODUwOF5BMl5BanBnXkFtZTcwMTU0NTIzMw@@._V1_SX300.jpg";
const Movie = ({ movie }) => {
const poster =
movie.Poster === "N/A" ? DEFAULT_PLACEHOLDER_IMAGE : movie.Poster;
return (
<div className="movie">
<h2>{movie.Title}</h2>
<div>
<img
width="200"
alt={`The movie titled: ${movie.Title}`}
src={poster}
/>
</div>
<p>({movie.Year})</p>
</div>
);
};
export default Movie;
6) 设置Search.js组件(有状态的)并导出
使用useState API,它让我们可以将 React 状态添加到函数组件中。它接收初始状态作为参数,返回一个数组,数组中包含当前状态、用以更新状态的函数。
将input字段作为当前状态传入。状态改变即onChange事件发生时,handleSearchInputChanges函数被调用,它将调用useState返回的用以更新状态的函数,将状态值更新为当前状态。
然后调用resetInputField函数,它将调用useState返回的用以更新状态的函数,将状态值更新为空字符串,从而实现清除输入字段的目的。
import React, { useState } from 'react';
const Search = ({ search }) => {
//input输入的状态值
const [searchValue, setSearchValue] = useState('');
//更新状态值为input输入的当前状态
const handleSearchInputChanges = (e) => {
setSearchValue(e.target.value);
}
//重置字段
const resetInputField = () => {
setSearchValue('');
}
//包含调用search函数的函数,该函数作为props传递给它
const callSearchFunction = (e) => {
e.preventDefault();
search(searchValue);
resetInputField();
}
//包含一个带有input元素、搜索button的表单
return (
<form className="search">
<input
onChange={handleSearchInputChanges}
type="text"
value={searchValue}
/>
<input
onClick={callSearchFunction}
type="submit"
value="SEARCH"
/>
</form>
);
}
export default Search;
7)更新App.js组件
传入样式和三个子组件
import React, { useState, useEffect } from 'react';
import '../App.css';
import Header from './Header';
import Movie from './Movie';
import Search from './Search';
设置初始页面显示的电影数组。
const MOVIE_API_URL = "https://www.omdbapi.com/?s=princess&apikey=4a3b711b";
定义App函数:
设置三个useState函数,以处理加载状态、从服务器获取电影数组、处理API请求可能遇到的错误。
使用useEffect函数,在组件挂载时从MOVIE_API_URL获取一次数据。
定义search函数,每次搜索新字段时,更新状态值,并传入搜索行为输入的新字段searchValue,以使用fetch发起HTTP请求。如下:
const App = () => {
//处理加载状态
const [loading, setLoading] = useState(true);
//从服务器获取电影数组
const [movies, setMovies] = useState([]);
//处理在发出API请求时可能发生的任何错误
const [errorMessage, setErrorMessage] = useState(null);
//在组件初始呈现期间调用API
//使用useEffect函数隔离副作用
useEffect(() => {
fetch(MOVIE_API_URL)
.then(res => res.json())
.then(result => {
setMovies(result.Search);
setLoading(false);
});
}, []);
const search = searchValue => {
setLoading(true);
setErrorMessage(null);
//处理API请求
fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
.then(res => res.json())
.then(result => {
if (result.Response === "True") {
setMovies(result.Search);
setLoading(false);
} else {
setErrorMessage(result.error);
setLoading(false);
}
});
};
const { movies, errorMessage, loading } = state;
return (
<div className="App">
<div className="m-container">
<Header text="HOOKED" />
<Search search={search} />
<p className="App-intro">
Sharing a few of our favourite movies
</p>
<div className="movies">{loading && !errorMessage ? (
<span>loading</span>
) : errorMessage ? (
<div className="errorMessage">{errorMessage}</div>
) : (
movies.map((movie, index) => (
<Movie key={`${index}-${movie.Title}`} movie={movie} />
))
)}
</div>
</div>
</div>
);
};
【优化】可以将获取展示电影数组封装进一个函数。如下:
const retrivedMovies =
loading && !errorMessage ? (
<span>loading</span>
) : errorMessage ? (
<div className="errorMessage">{errorMessage}</div>
) : (
movies.map((movie, index) => (
<Movie key={`${index}-${movie.Title}`} movie={movie} />
))
)
return (
<div className="App">
<div className="m-container">
<Header text="HOOKED" />
<Search search={search} />
<p className="App-intro">Sharing a few of our favourite movies</p>
<div className="movies">{retrivedMovies}</div>
</div>
</div>
);
【优化】由于我们使用了三个相互关联的useState函数,可以考虑改为使用useReducer函数。axios中数据的字符串化是自动完成的,可以考虑将获取数据的方式由fetch改为axios。
添加gif加载图片。
import { initialState, reducer } from '../store/reducer';
import spinner from '../image/giphy.gif';
import axios from 'axios';
const initialState = {
loading: true,
movies: [],
errorMessage: null
};
const reducer = (state, action) => {
switch (action.type) {
case "SEARCH_MOVIES_REQUEST":
return {
...state,
loading: true,
errorMessage: null
};
case "SEARCH_MOVIES_SUCCESS":
return {
...state,
loading: false,
movies: action.payload
};
case "SEARCH_MOVIES_FAILURE":
return {
...state,
loading: false,
errorMessage: action.error
};
default:
return state;
}
};
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
axios.get(MOVIE_API_URL).then(result => {
dispatch({
type: "SEARCH_MOVIES_SUCCESS",
payload: result.data.Search
});
});
}, []);
const search = searchValue => {
dispatch({
type: "SEARCH_MOVIES_REQUEST"
});
axios(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
.then(result => {
if (result.data.Response === "True") {
dispatch({
type: "SEARCH_MOVIES_SUCCESS",
payload: result.data.Search
});
} else {
dispatch({
type: "SEARCH_MOVIES_FAILURE",
error: result.data.Error
});
}
});
};
const { movies, errorMessage, loading } = state;
const retrivedMovies =
loading && !errorMessage ? (
<img className="loading" src={spinner} alt="Loading spinner" />
) : errorMessage ? (
<div className="errorMessage">{errorMessage}</div>
) : (
movies.map((movie, index) => (
<Movie key={`${index}-${movie.Title}`} movie={movie} />
))
)
return (
<div className="App">
<div className="m-container">
<Header text="HOOKED" />
<Search search={search} />
<p className="App-intro">Sharing a few of our favourite movies</p>
<div className="movies">{retrivedMovies}</div>
</div>
</div>
);
};
export default App;
8) 封装reducer函数
在src下新建store文件夹,在其中新建reducer文件夹,然后新建index.js文件。
将如下代码剪切到文件中并导出:
export const initialState = {
loading: true,
movies: [],
errorMessage: null
};
export const reducer = (state, action) => {
switch (action.type) {
case "SEARCH_MOVIES_REQUEST":
return {
...state,
loading: true,
errorMessage: null
};
case "SEARCH_MOVIES_SUCCESS":
return {
...state,
loading: false,
movies: action.payload
};
case "SEARCH_MOVIES_FAILURE":
return {
...state,
loading: false,
errorMessage: action.error
};
default:
return state;
}
};