跟着尚硅谷的天禹老师学习React
看视频可以直接点击 b站视频地址
public中最重要的文件
DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React Apptitle>
head>
<body>
<noscript>You need to enable JavaScript to run this app.noscript>
<div id="root">div>
body>
html>
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals'; // 用于记录页面性能监测
ReactDOM.render(
// 包裹stricMode,来检查App里不合理的地方,比如字符串的ref等
<React.StrictMode>
<App />
</React.StrictMode>,
// 将App组件挂载到public里的index.html中写好的那个root节点上
document.getElementById('root')
);
reportWebVitals();
export
/**
* 这个模块用了多种暴露的形式:
* 1、使用默认暴露,暴露出了React
* 2、使用分别暴露,暴露除了Component
*/
const React = {a:1,b:2}
export class Component {}
React.Component = Component
export default React
import
后边的Component并非解构赋值,而是从分别暴露中取到,与 const { Component } = React是两码事
// 创建“外壳”组件
import React, { Component } from "react";
// 后边的Component并非解构赋值,而是从分别暴露中取到,与 const { Component } = React是两码事
class App extends Component {
render() {
return <div>Hello,React</div>;
}
}
// 暴露App组件
export default App;
目录
components文件夹内存组件,index.js为程序入口,App.js为根组件。下面省略了css的模块。
详细代码
index.js
/* index.js */
// 引入React核心库
import React from "react";
// 引入ReactDOM
import ReactDOM from "react-dom";
// 引入App组件
import App from './App'
// 渲染App到页面
ReactDOM.render(<App/>,document.getElementById('root'))
App.js
/* App.js */
// 创建“外壳”组件
// 后边的Component并非解构赋值,而是从分别暴露中取到,与 const { Component } = React是两码事
import React, { Component } from "react";
// 引入Hello组件
import Hello from "./components/Hello/Hello"; // js和jsx文件在React中可以不屑后缀
// 引入Welcome组件
import Welcome from "./components/Welcome";
// 创建并暴露App组件
export default class App extends Component {
render() {
return (
<div>
<Hello />
<Welcome />
</div>
);
}
}
Hello.jsx
/* Hello.jsx */
import React, { Component } from "react";
import './Hello.css'
export default class Hello extends Component {
render() {
return <h1 className="title">Hello,React</h1>;
}
}
Welcome/index.js
import React, { Component } from "react";
import './index.css'
export default class Welcome extends Component{
render(){
return(
<h2 className="demo">Welcome</h2>
)
}
}
注意两个地方:
1、引入组件有两种写法:一种是用组件名来命名,一种是组件名做文件夹然后js和css模块使用index,后者在引入时比较方便一点。
2、最好将组件都写成jsx的后缀,但是App最好写成js后缀。
3、组件最好首字母大写
.title{
background-color: blue;
}
.demo{
background-color: skyblue;
}
在刚刚的代码中出现了问题,如果两个class同名那么背景色只能有一个起作用。我们可以这样修改:
import hello from './Hello.module.css'
export default class Hello extends Component {
render() {
return <h1 className={hello.title}>Hello,React</h1>;
}
}
或者也可以用less或者scss写的时候,外面套一个名字。
发现创建一个组件,需要手动输入大量的类似模板的代码,最好可以自动地生成这些固定的代码。
rcc:react class component
rfc:react function component
除此之外可以在这个插件的Basic Methods里面查看。
实现一个和上图一样的todolist,包含添加todo项、删除、全选、清除已完成、勾选完成等功能。
index.js
/* index.js */
// 引入React核心库
import React from "react";
// 引入ReactDOM
import ReactDOM from "react-dom";
// 引入App组件
import App from './App'
// 渲染App到页面
ReactDOM.render(<App/>,document.getElementById('root'))
App.js
/* App.js */
import React, { Component } from "react";
import Header from "./Header";
import Footer from "./Footer";
import List from "./List";
export default class App extends Component {
/**
* 状态在哪里,操作状态的方法就应该放在哪里
*/
// 初始化状态
state = {
todos: [
{ id: 1, name: "吃饭", done: true },
{ id: 2, name: "睡觉", done: true },
{ id: 3, name: "敲代码", done: false },
{ id: 4, name: "逛街", done: false },
],
};
// 用于添加一个todo,接收的是一个对象
addTodo = (todo) => {
const { todos } = this.state;
// React不推荐直接用unshift或者push来操作数组,最好是函数式编程
this.setState({ todos: [todo, ...todos] });
};
// 用于更新一个todo对象
updateTodo = (id, done) => {
const { todos } = this.state;
const newTodos = todos.map((todo) => {
if (todo.id === id) return { ...todo, done };
else return todo;
});
this.setState({ todos: newTodos });
};
// 用于删除一个todo项
deleteTodo = (id) => {
const { todos } = this.state;
const newTodos = todos.filter((todo) => {
return todo.id !== id;
});
this.setState({ todos: newTodos });
};
// 用于全选todo
checkAllTodos = (flag) => {
const { todos } = this.state;
const newTodos = todos.map((todo) => {
return { ...todo, done: flag };
});
this.setState({ todos: newTodos });
};
// 删除所有已完成todo
clearAllDone = () => {
const { todos } = this.state;
const newTodos = todos.filter((todo) => {
return !todo.done;
});
this.setState({ todos: newTodos });
};
render() {
const { todos } = this.state;
return (
<div>
<Header addTodo={this.addTodo} />
<List
todos={todos}
updateTodo={this.updateTodo}
deleteTodo={this.deleteTodo}
/>
<Footer todos={todos} clearAllDone={this.clearAllDone} checkAllTodos={this.checkAllTodos} />
</div>
);
}
}
Header.jsx
/* Header.jsx */
import React, { Component } from "react";
import { nanoid } from "nanoid";
import PropTypes from "prop-types";
export default class Header extends Component {
// 对接收的props进行类型、必要性的限制
static propTypes = {
addTodo: PropTypes.func.isRequired,
};
// 键盘事件的回调
handleKeyUp = (event) => {
// 解构赋值两个值
const { keyCode, target } = event;
// 判断是否是回车键
if (keyCode !== 13) {
return;
}
if (target.value.trim() === "") {
alert("不能为空");
return;
}
const { addTodo } = this.props;
const todo = { id: nanoid(), name: target.value, done: false };
addTodo(todo);
target.value = "";
};
render() {
return (
<input
onKeyUp={this.handleKeyUp}
type="text"
placeholder="请输入新的任务名称,按回车确认"
/>
);
}
}
List.jsx
/* List.jsx */
import React, { Component } from "react";
import Item from "../Item";
import PropTypes from "prop-types";
export default class List extends Component {
// 对接收的props进行类型、必要性的限制
static propTypes = {
todos: PropTypes.array.isRequired,
updateTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired,
};
render() {
const { todos, updateTodo, deleteTodo } = this.props;
return (
<ul>
{todos.map((item) => {
return (
<Item
key={item.id}
{...item}
updateTodo={updateTodo}
deleteTodo={deleteTodo}
/>
);
})}
</ul>
);
}
}
Footer.jsx
/* Footer.jsx */
import React, { Component } from "react";
import PropTypes from "prop-types";
export default class Footer extends Component {
// 对接收的props进行类型、必要性的限制
static propTypes = {
checkAllTodos: PropTypes.func.isRequired,
todos: PropTypes.array.isRequired,
clearAllDone:PropTypes.func.isRequired
};
// 全选todo
handleCheckAll = (event) => {
this.props.checkAllTodos(event.target.checked);
};
// 清除所有已完成todo
handleCleatAllDone = () => {
this.props.clearAllDone();
};
render() {
const { todos } = this.props;
const doneCount = todos.reduce((preValue, todo) => {
return preValue + (todo.done ? 1 : 0);
}, 0);
const totalCount = todos.length;
return (
<div>
<label>
<input
type="checkbox"
checked={totalCount === doneCount && totalCount !== 0}
onChange={this.handleCheckAll}
/>
</label>
<span>
<span>已完成{doneCount}</span>/ 全部{totalCount}
</span>
<button
style={{ marginLeft: "50px" }}
onClick={this.handleCleatAllDone}
>
清除已完成任务
</button>
</div>
);
}
}
Item.jsx
/* Item.jsx */
import React, { Component } from "react";
import PropTypes from "prop-types";
export default class Item extends Component {
state = {
mouseOnHere: false, // 当前鼠标是否在本元素上
};
// 对接收的props进行类型、必要性的限制
static propTypes = {
name: PropTypes.string.isRequired,
done:PropTypes.bool.isRequired,
updateTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired,
};
// 鼠标移入移出的事件处理函数
handleMouse = (isEnter) => {
return () => {
this.setState({ mouseOnHere: isEnter });
};
};
// 勾选或取消某一个todo
handleCheck = (id) => {
return (event) => {
this.props.updateTodo(id, event.target.checked);
};
};
// 删除一个项的回调
handleDelete = (id) => {
if (window.confirm("确定删除吗?")) {
this.props.deleteTodo(id);
}
};
render() {
const { id, name, done } = this.props;
const { mouseOnHere } = this.state;
return (
<li
style={{ backgroundColor: mouseOnHere ? "grey" : "white" }}
onMouseLeave={this.handleMouse(false)}
onMouseEnter={this.handleMouse(true)}
>
<label>
<input
type="checkbox"
checked={done}
onChange={this.handleCheck(id)}
/>
<span>{name}</span>
</label>
<button
onClick={() => this.handleDelete(id)}
style={{ display: mouseOnHere ? "block" : "none" }}
>
删除
</button>
</li>
);
}
}
1、拆分组件、实现静态组件,注意:className和style写法
2、动态初始化列表:如何确定将数据放在哪个组件的state中?
3、关于父子组件通信:
4、注意defaultChecked和checked的区别,类似还有:defaultValue和value
5、状态在哪里,操作状态的方法就放在哪里。
在packge.json文件中加入下面这段代码:
"proxy":"http://localhost:5000"
加入这行代码以后,当我们请求前端的地址时(React脚手架一般是在3000端口起服务),所有的请求当3000端口上没有的资源或者服务,会被转发至localhost:5000上。
说明:
在项目src目录下创建一个名为setUpProxy的文件,React会自动读取其中的配置。
// 注意这里必须使用commonJS的写法来写
const { default: axios } = require("axios");
const proxy = require("http-proxy-middleware");
module.exports = function (app) {
app.use(
proxy("/api1", { // 遇见“/api1”为前缀的请求,就会触发该代理配置
target: "http://localhost:5000", // 请求转发的真实地址
changeOrigin: true, // 控制服务器收到的请求头中host字段的值,甚至服务端将拿到自己同源的host
/*
changeOrigin为true,服务端收到的请求头中的host为localhost:5000
changeOrigin为false,服务器收到的请求头中的host为localhost:3000
changeOrigin默认为false,但我们一般将changeOrigin设置为true
*/
pathRewrite: {"^api1": ""}, // 重写请求路径,相当于把原来请求地址中的api1给消掉。
}),
proxy('/api2',{
target:'http://localhost:5001',
changeOrigin:true,
pathRewrite:{'^/api2':''}
})
);
};
axios.get('http:localhost:3000/api1/students')
// 端口为5000的服务器得到的请求地址为,localhost:5000/students
// 上面的pathRewrite将 http:localhost:3000/api1/students 转换为了http:localhost:5000/students,即将“/api1”替换成空字符串
// 上面的changeOrigin欺骗服务器,让其认为请求是来自localhost:5000发出的
说明:
这里拆分出两个组件List和Search。
setUpProxy.js
/* setUpProxy.js */
// 注意这里和教程不同,因为这个中间件的版本换了,需要以createProxyMiddleWare的方式来创建Proxy
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = function (app) {
app.use(
createProxyMiddleware("/api1", {
//遇见/api1前缀的请求,就会触发该代理配置
target: "http://localhost:5000", //请求转发给谁
changeOrigin: true, //控制服务器收到的请求头中Host的值
pathRewrite: { "^/api1": "" }, //重写请求路径(必须)
})
);
};
App.js
/* App.js */
import React, { Component } from "react";
import List from "./components/List";
import Search from "./components/Search";
export default class App extends Component {
state = {
users: [], // 初始化状态,users为数组
isFirst: true, // 是否为第一次加载页面
isLoading: false, // 是否处于加载中
err: "", // 存储相关的错误信息
};
// 更新app的state
updateAppState = (stateObj) => {
this.setState(stateObj);
};
render() {
return (
<div>
<Search updateAppState={this.updateAppState} />
<List {...this.state} />
</div>
);
}
}
List.jsx
/* List.jsx */
import React, { Component } from "react";
import "./index.css";
import PropTypes from "prop-types";
export default class List extends Component {
static propTypes = {
users: PropTypes.array.isRequired,
};
render() {
const { users, isFirst, isLoading, err } = this.props;
console.log(users, isFirst, isLoading, err);
return (
<div className="">
<div className="row">
{isFirst ? (
<h1>欢迎使用</h1>
) : isLoading ? (
<h1>loading...</h1>
) : err ? (
<h1 style={{ color: "red" }}>{err}</h1>
) : (
users.map((user) => {
return (
<div className="card" key={user.id}>
<a href={user.html_url} rel="noreferrer" target="_blank">
<img src={user.avatar_url} alt="avatar"></img>
<p className="card-text">{user.login}</p>
</a>
</div>
);
})
)}
)
</div>
</div>
);
}
}
Search.jsx
/* Search.jsx */
import axios from "axios";
import React, { Component } from "react";
import PropTypes from "prop-types";
export default class Search extends Component {
search = () => {
// 连续解构赋值,并将value重命名为keyword
const {
keywordElement: { value: keyword },
} = this;
this.props.updateAppState({ isFirst: false, isLoading: true });
// 因为访问的就是localhost:3000地址,所以写下路径即可
axios.get(`/api1/search/users2?q=${keyword}`).then(
(response) => {
this.props.updateAppState({
isLoading: false,
users: response.data.items,
err:""
});
},
(error) => {
this.props.updateAppState({ isLoading: false, err: error.message });
}
);
};
static propTypes = {
updateAppState: PropTypes.func.isRequired,
};
render() {
return (
<div>
<h1>Search Github Users</h1>
<input
ref={(c) => {
this.keywordElement = c;
}}
placeholder="输入想要搜索的用户"
></input>
<button onClick={this.search}>搜索</button>
</div>
);
}
}
PubSub库(pubsub-js)
yarn add pubsub-js
如果因为java原因报出了“找不到主类balabala”的错误,就将yarn换成yarnpkg也可以使用yarn的指令。
有些类似Vue的总线传参($emit,$on)。
实际上App中的state、updateAppState都是跟其本身没有太多关系的,如果能够让Search和List组件自己通信,就可以省略掉App中这些无用的代码,提高内聚性。
1、删除了App的state和用于更新的updateAppState,让App更轻
2、将状态移入List组件中自己维护
3、Search组件发布消息,List组件订阅消息
4、List和Search组件引入pubsub-js库来完成消息订阅发布流程
App.js
/* App.js */
import React, { Component } from "react";
import List from "./components/List";
import Search from "./components/Search";
export default class App extends Component {
render() {
return (
<div>
<Search />
<List />
</div>
);
}
}
List.jsx
/* List.jsx */
import React, { Component } from "react";
import "./index.css";
import PubSub from "pubsub-js";
export default class List extends Component {
state = {
users: [], // 初始化状态,users为数组
isFirst: true, // 是否为第一次加载页面
isLoading: false, // 是否处于加载中
err: "", // 存储相关的错误信息
};
componentDidMount() {
// 挂载后立刻订阅updateAppState消息
this.token = PubSub.subscribe("updateAppState", (msg, data) => {
this.setState(data);
});
}
componentWillUnmount() {
// 取消订阅
PubSub.unsubscribe(this.token);
}
render() {
const { users, isFirst, isLoading, err } = this.state;
console.log(users, isFirst, isLoading, err);
return (
<div className="">
<div className="row">
{isFirst ? (
<h1>欢迎使用</h1>
) : isLoading ? (
<h1>loading...</h1>
) : err ? (
<h1 style={{ color: "red" }}>{err}</h1>
) : (
users.map((user) => {
return (
<div className="card" key={user.id}>
<a href={user.html_url} rel="noreferrer" target="_blank">
<img src={user.avatar_url} alt="avatar"></img>
<p className="card-text">{user.login}</p>
</a>
</div>
);
})
)}
)
</div>
</div>
);
}
}
Search.jsx
/* Search.jsx */
import axios from "axios";
import React, { Component } from "react";
import PubSub from "pubsub-js";
export default class Search extends Component {
updateAppState = (stateObj) => {
this.setState(stateObj);
};
search = () => {
// 连续解构赋值,并将value重命名为keyword
const {
keywordElement: { value: keyword },
} = this;
// 因为访问的就是localhost:3000地址,所以写下路径即可
axios.get(`/api1/search/users2?q=${keyword}`).then(
(response) => {
// 发布updateState消息
PubSub.publish('updateAppState',{
isLoading: false,
users: response.data.items,
err: "",
isFirst:false
});
},
(error) => {
// 发布updateState消息
PubSub.publish('updateAppState',{ isLoading: false, err: error.message });
}
);
};
render() {
return (
<div>
<h1>Search Github Users</h1>
<input
ref={(c) => {
this.keywordElement = c;
}}
placeholder="输入想要搜索的用户"
></input>
<button onClick={this.search}>搜索</button>
</div>
);
}
}
重写了search,使用window的fetch属性重新实现。
关注点分离是日常生活和生产中广泛使用的解决复杂问题的一种系统思维方法。大体思路是,先将复杂问题做合理的分解,再分别仔细研究问题的不同侧面(关注点),最后综合各方面的结果,合成整体的解决方案。这是一种重要的计算思维。
search = async()=>{
//获取用户的输入(连续解构赋值+重命名)
const {keyWordElement:{value:keyWord}} = this
//发送请求前通知List更新状态
PubSub.publish('atguigu',{isFirst:false,isLoading:true})
//#region 发送网络请求---使用axios发送
/* axios.get(`/api1/search/users2?q=${keyWord}`).then(
response => {
//请求成功后通知List更新状态
PubSub.publish('atguigu',{isLoading:false,users:response.data.items})
},
error => {
//请求失败后通知App更新状态
PubSub.publish('atguigu',{isLoading:false,err:error.message})
}
) */
//#endregion
//发送网络请求---使用fetch发送(未优化)
/* fetch(`/api1/search/users2?q=${keyWord}`).then(
response => {
console.log('联系服务器成功了');
return response.json()
},
error => {
console.log('联系服务器失败了',error);
return new Promise(()=>{})
}
).then(
response => {console.log('获取数据成功了',response);},
error => {console.log('获取数据失败了',error);}
) */
//发送网络请求---使用fetch发送(优化)
try {
const response= await fetch(`/api1/search/users2?q=${keyWord}`)
const data = await response.json()
console.log(data);
PubSub.publish('atguigu',{isLoading:false,users:data.items})
} catch (error) {
console.log('请求出错',error);
PubSub.publish('atguigu',{isLoading:false,err:error.message})
}
}
1、发http请求不止可以使用xhr(axios、jquery等的底层),还可以用fetch
2、尽管fetch并非xhr方法,但在浏览器network的选项卡中依然可以看到,因为xhr只是简写,选项卡名称为XHR and Fetch。
3、fetch返回的promise,只有某些极其特殊的错误(比如断网了)才会转为rejected,像404等错误依然会被认为是resolved。如果需要拿到数据,可以调用返回的response的json()方法,即response.json(),返回一个新的promise对象。
4、对于promise的错误处理,可以使用异常穿透或者使用async/await以及try-catch来处理,这样代码量会减少。
5、fetch在实际工作中不太常见。
const obj = {a:{b:1}
const { a } = obj // 普通结构赋值
const { a:{ b }} = obj // 连续解构赋值
const { a:{ b:c }} = obj // 将b解构赋值出来并重命名为c