跟着尚硅谷的天禹老师学习React
看视频可以直接点击 b站视频地址
React脚手架
目录结构
public目录
index.html
public中最重要的文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- %PUBLIC_URL% 代表当前项目的Public文件路径也可以写成相当路径 -->
<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"
/>
<!-- 把网站添加到苹果手机主屏幕以后,那个APP的图标 -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- 应用“加壳”后,指定这个加壳后APP的配置 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<!-- 当浏览器不支持js时,展示这个标签的内容 -->
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- 这个root元素可以看成前面一直写的container -->
<div id="root"></div>
</body>
</html>
src目录
index.js
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();
写一个小案例
对于import和export的一点补充
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同名那么背景色只能有一个起作用。我们可以这样修改:
- 将组件的css,写成【组件名.module.css】,这样Webpack会将其视为一个模块
- 在jsx文件中保存这个变量
import hello from './Hello.module.css'
- 在className中使用上面的变量获取样式
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
产品需求
实现一个和上图一样的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中?
- 某个组件使用:放在其自身的state中
- 某些组件使用:放在他们共同的父组件的state中(这被官方称为状态提升)
3、关于父子组件通信:
- 父组件给子组件传值:props
- 子组件给父组件传值:要求父组件提前定义好一个函数
4、注意defaultChecked和checked的区别,类似还有:defaultValue和value
5、状态在哪里,操作状态的方法就放在哪里。
脚手架配置代理
当只有一个服务器时
在packge.json文件中加入下面这段代码:
"proxy":"http://localhost:5000"
加入这行代码以后,当我们请求前端的地址时(React脚手架一般是在3000端口起服务),所有的请求当3000端口上没有的资源或者服务,会被转发至localhost:5000上。
说明:
- 优点:配置简单,前端请求资源时可以不加任何前缀
- 缺点:不能配置多个代理
- 工作方式:当请求了3000端口上不存在的资源时,那么请求会被转发到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发出的
说明:
- 优点:可以配置多个代理,可以灵活地控制请求是否走代理
- 缺点:配置繁琐,前端请求资源时必须加前缀
案例:github搜索
拆分组件
这里拆分出两个组件List和Search。
状态提升版代码
目录结构
这里setUpProxy写错了
代码
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>
);
}
}
fetch版本(关注点分离)
重写了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在实际工作中不太常见。
总结
- 设计状态时一定要考虑全面,例如带有网络请求的组件,要考虑请求失败应该如何处理
- ES6知识点:解构赋值及重命名
const obj = {a:{b:1}
const { a } = obj // 普通结构赋值
const { a:{ b }} = obj // 连续解构赋值
const { a:{ b:c }} = obj // 将b解构赋值出来并重命名为c
- 消息订阅与发布
- 先订阅,再发布
- 适用于任何关系组件的通信
- 推荐在componentDidMount中订阅,在componentWillUnmount中取消订阅
- fetch发送请求(关注分离的设计思想)