1.虚拟DOM
虚拟DOM本质是一个Object类型的对象
//1.创建虚拟DOM,使用JSX语法,babel会翻译为原生js
const VDOM = (
<h1 id="title">
<span>Hello, React</span>
</h1>
)
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM, document.getElementById("test"))
jsx语法
1.定义虚拟DOM时,文本字符串不需要写引号
2.引用js定义的变量,要使用 {} ,如:<span>{myData}</span>
3.样式的引入,属性名不是class,而是className
4.在标签中直接定义样式,需要用{{}},如:<span style={{color:'white', fontSize:'29px'}}>{myData}</span>
5.引入react组件时,使用组件名作为标签名,首字母要大写,如:<Mycomponent>
2.遍历数据
const data = ['Angular', 'React', 'Vue']
const VDOM = (
<div>
<h1>前端框架列表</h1>
<ul>{
data.map((item, index) => { //千万不要用index作为节点的key,这里只是为了方便
return <li key={index}>{item}</li> //取遍历数据的节点必须设置key属性
})
}</ul>
</div>
)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-csxwrAmh-1617067323356)(C:\Users\Homelander\OneDrive\学习笔记\笔记中的图片\image-20210310163426177.png)]
3.两种组件
//1.创建函数式组件,函数返回一个虚拟DOM
function Demo() {
return <h2>这是一个Demo组件</h2>
}
//2.渲染组件到面页
ReactDOM.render(<Demo/>, document.getElementById('test'))
//1.创建类式组件,类中的render函数返回一个虚拟DOM
class Demo extends React.Component {
render() {
return <h2>我是一个Demo组件</h2>
}
}
//2.渲染组件到面页
ReactDOM.render(<Demo/>, document.getElementById('test'))
/*
ReactDOM渲染组件时发现该组件是类定义的,会自动new出此类的实例,并调用该实例对象中的render方法
*/
4.事件,this指向问题
详见:https://www.jianshu.com/p/c1ee12a328d2
//原生绑定事件的三种方式
<body>
<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>
<button οnclick="demo()">按钮3</button>
<script>
方式1:不建议使用
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click', () => {
alert('按钮1被点击了')
})
方式2:不建议使用
const btn2 = document.getElementById('btn2')
btn2.onclick = () => {
alert('按钮2被点击了')
}
方式3:更推荐的写法
function demo() {
alert('按钮3被点击了')
}
</script>
</body>
//react绑定事件的方法
<script>
class Weather extends React.Component {
/*在react中,构造器仅用于以下两种情况:
1.通过this.state赋值对象来初始化内部state
2.为事件处理函数绑定实例
如果不存在以上两种情况的使用,可以不写构造器
*/
constructor(props) {
super(props) //必须调用super
this.state = {isHot:false}
this.demo = this.demo.bind(this) //重点,这行代码就将demo方法绑定到了当前对象实例上
}
render() {
const {isHot} = this.state
return <h1 onClick={this.demo}>今天天气很{isHot ? '热' : '冷'}</h1>
}
//demo方法放在了类的proto对象中,以供当前实例对象使用
demo() {
console.log('标题被点击了')
console.log(this) //注意:只有在当前类的实例对象调用demo方法,this才能指向当前对象
//如果绕过实例,直接调用demo方法,this将指向window实例
//如果方法开启了'use strict'模式,this将为undefined,而react类中定义的方法会默认开启use strict
//详见 https://www.bilibili.com/video/BV1wy4y1D7JT?p=15
//这里想要让this指向当前类对象实例,需要在构造器中添加this.demo = this.demo.bind(this)
//当在箭头函数中使用this时,箭头函数会使用其外层的this作为自己的this
}
}
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
5.数据绑定
react、vue都是单向数据流的框架,即数据的流向只能通过props由外层到内层 一层一层往里传递
vue可以通过v-modle实现双向绑定,使view层和model层的映射关系实时更新
react是单向数据绑定,view层的修改需要使用react内置的api方法,setState
demo() {
const isHot = this.state.isHot
//只有setState,数据的变化才能在面页上更新,setState只会更新state中指定属性的值,不影响其他属性
this.setState({isHot: !isHot})
}
6.精简初始化代码
//开发时的写法
//react中的类,可以直接以赋值语句的形式定义属性,new实例时会自动初始化类内部的所有属性
class Weather extends React.Component {
state = {
isHot: false,
wind: '大风'
}
render() {
const {isHot, wind} = this.state
return (
<h1 onClick={this.demo}>今天天气很{isHot ? '热' : '冷'},{wind}</h1>
)
}
//将demo方法定义为属性,这里必须使用箭头函数,this的才能正确指向当前类的实例
demo = () => {
const isHot = this.state.isHot
this.setState({
isHot: !isHot
})
}
}
7.组件标签属性的指定
class Person extends React.Component {
render() {
const {name, age, sex} = this.props //props属性是只读的,只能读取属性值,不能写入属性值
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
<li>性别:{sex}</li>
</ul>
)
}
//对组件标签属性类型进行限制
static propTypes = { //注意,必须使用static修饰,表示给类添加属性,而非给实例对象添加属性
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number
}
//给组件标签属性指定默认值
static defaultProps = {
sex: '跨性别者',
age: 18
}
}
//渲染组件到面页
ReactDOM.render(<Person name="小红" age={19} />, document.getElementById('test1'))
ReactDOM.render(<Person name="小蓝" sex="男"/>, document.getElementById('test2'))
const p = {name: '小白', age: 20, sex: '女'}
ReactDOM.render(<Person {...p}/>, document.getElementById('test2'))
8.ref的使用
ref是React提供的用来操纵React组件实例或者DOM元素的接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NJTy6Zm2-1617067323359)(C:\Users\Homelander\OneDrive\学习笔记\笔记中的图片\image-20210310120247211.png)]
上面的ref回调函数写法也可以写成内联形式的,效果一样,如下,开发时写下面这种能简洁点,就不用再多定义一个函数
render() {
return (
<div>
{<!-- 这样在函数中就可以使用this.textInput直接操作input节点 -->}
<input type="text" ref={(element) => {this.textInput = element;}}></input>
</div>
)
}
9.受控组件和非受控组件
非受控组件
class Login extends React.Component {
handlerSubmit = (event) => {
event.preventDefault() //阻止表单提交
const {username, password} = this
alert(`用户名:${username.value},密码:${password.value}`)
}
render() {
return(
<form onSubmit={this.handleSubmit}>
用户名:<input type="text" name="username" ref={(ele) => {this.username = ele;}}></input>
密码:<input type="password" name="password" ref={(ele) => {this.password = ele;}}></input>
<button>登录</button>
</form>
)
}
ReactDOM.render(<Login/>, document.getElementById('test'))
}
受控组件(类似vue中的双向数据绑定,更建议使用这种写法)
class Login extends React.Component {
//初始化状态
state = {
username: '',
password: ''
}
//保存表单数据到状态中
/*
高阶函数:符合以下两条中的任意一条,就是高阶函数
1.一个函数接收的参数也是一个函数
2.一个函数返回值也是一个函数
常见的如:Promise、setTimeout、arr.map()等等
函数的柯里化:通过函数调用返回函数,经过多次参数传递最后统一处理返回结果的函数编写方式
简单来说,就是调用函数时,如果函数后加了小括号(一般是因为要传递参数),就要柯里化,如: onClick={this.doClick(p)}
如果函数不需要传递参数,那么可以不加小括号调用,就不用柯里化,如: onClick={this.doClick}
*/
saveFormData = (param, event) => {
return (event) => {
this.setState({[param]: event.target.value})
}
}
//表单提交的回调函数
handleSubmit = (event) => {
event.preventDefault() //阻止表单提交
}
render() {
return(
<form onSubmit={this.handleSubmit}>
用户名:<input type="text" name="username" onChange={this.saveFormData('username')}></input>
密码:<input type="password" name="password" onChange={this.savePassword('password')}></input>
<button>登录</button>
</form>
)
}
ReactDOM.render(<Login/>, document.getElementById('test'))
}
为了避免函数柯里化,开发时常用的写法
class Login extends React.Component {
//初始化状态
state = {
username: '',
password: ''
}
//保存表单数据到状态中
saveFormData = (param, event) => {
this.setState({[param]: event.target.value})
}
//表单提交的回调函数
handleSubmit = (event) => {
event.preventDefault() //阻止表单提交
}
render() {
return(
<form onSubmit={this.handleSubmit}>
用户名:<input type="text" name="username"
onChange={(event) => {this.saveFormData('username', event)}></input>
密码:<input type="password" name="password"
onChange={(event) => {this.savePassword('password', event)}></input>
<button>登录</button>
</form>
)
}
ReactDOM.render(<Login/>, document.getElementById('test'))
}
10.react的生命周期
实际开发中最重要的且最常用的就三个钩子函数
- render:初始化渲染或更新渲染时调用
- componentDidMount:开启监听,发送ajax请求
- componentWillUnmount:清理定时器
/*
钩子函数
1.初始化阶段:由ReactDOM.render()触发
constructor() → componentWillMount() → render() → componentDidMount()
2.更新阶段:由组件内部this.setState()或父组件render触发
shouldComponentUpdate() → componentWillUpdate() → render() → componentDidUpdate()
3.卸载阶段:由ReactDOM.unmountComponentAtNode()触发
componentWillUnmount()
*/
//组件挂载前执行一次
componentWillMount() { //不常用,即将弃用
}
//父组件render后执行一次
componentWillReceiveProps() { //不常用,即将弃用
}
//setState()完成后,执行该函数,返回布尔值,不写该函数,react会默认执行并返回true
shouldComponentUpdate() {
}
//shouldComponentUpdate()或forceUpate()完成后,执行该函数,forceUpate可以使不改变状态的情况下调用该函数
componentWillUpdate() { //不常用,即将弃用
}
//渲染(挂载)组件
render() { //必须使用
}
//组件挂载完毕后执行,且只执行一次,类似于vue的mounted
componentDidMount() { //常用,一般在其中做一些初始化的工作,如开启定时器、发送网络请求、订阅消息
}
//组件如果被卸载,则在卸载前执行一次
componentWillUnmount() { //常用,一般在其中做一些收尾的工作,如关闭定时器、取消订阅消息
}
11.父子通信
父传子,使用props
//1.父组件中,render函数里用标签引入它的子组件,在标签中可以定义属性参数,实现父组件传递子组件
class Father extends React.Component {
state = {
data: {name, age, gender}
}
render() {
return <Child data={this.state.data}> {/*将父组件中的data属性传给子组件*/}
}
}
//2.子组件中,使用props接收来自父组件的属性参数
class Child extends React.Component {
const {data} = this.props
render() {
return <button>{data.name}</button>
}
}
子传父,使用函数
//1.父组件中,render函数里的标签传入一个函数,函数的参数即为需要传递的属性
class Father extends React.Component {
state = {
data: {name, age, gender}
}
//该函数被传递给子组件,子组件通过调用函数,传递参数param给父组件
changeData = (param) => {
//处理子组件传递过来的参数,最后更新状态
this.name = param.name
}
render() {
return <Child changeData={this.changeData}> {/*将父组件中的函数传给子组件*/}
}
}
//2.子组件中,使用props接收来自父组件的函数
class Child extends React.Component {
change = () => {
axios.get(`/api/change/name`).then(response => {
this.props.changeData(response.data.name)
})
}
render() {
return <button onClick={this.change}></button>
}
}
12.代理
为了解决跨域问题,项目可能会设置有代理的服务器,负责转发请求
在src路径下创建setupProxy.js文件
//先安装 npm install http-proxy-middleware --save
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
//参数 /api 表示请求url中必须包含 /api 才能被拦截到并触发代理
app.use(createProxyMiddleware('/api', {
//代理服务器1的地址
target: 'http://121.36.69.149:8033',
//控制后端收到的请求头中Host的值,一般设置为true,这样后端得到的是代理服务器的地址而非真实请求源地址,默认为flase
changeOrigin: true,
//请求发送到后端,把后端接收到的url中的 /api 去掉,如果不设置此属性,后端@RequestMapping就需要把 /api 加上
pathRewrite: {'^/api': ''},
//如果是https接口,这个参数需要设置为true
secure: false
}));
//代理服务器2
app.use(createProxyMiddleware('/api2', {
//代理服务器2的地址
target: 'http://121.36.69.149:8034',
changeOrigin: true,
pathRewrite: {'^/api2': ''},
secure: false
}));
};
13.消息订阅与发布
为了避免组件之间传递数据的繁琐性操作,引入消息中间件 PubSubJS,极大的简化任意组件之间的通信
https://www.npmjs.com/package/pubsub-js
发布消息的组件
import PubSub from 'pubsub-js'
export default class Pub extends Component {
change = () => {
//发送请求前先发布消息,修改订阅消息组件state中的某些需要改变状态的属性
PubSub.publish('msgname', {
isLoading: true
})
axios.get(`api/change/name`).then(response => {
//请求收到响应数据后,使用消息发布修改订阅消息组件的state
PubSub.publish('msgname', {
name: response.data.name
age: response.data.age
isLoading: false
})
})
}
}
订阅消息的组件
import PubSub from 'pubsub-js'
export default class Sub extends Component {
state = {
name: '',
age: 0,
isLoading: false
}
componentDidMount() {
this.token = PubSub.subscribe('msgname', (msg, data) => {
this.setState(data) //从消息发布组件处获取最新的state数据,更新state
})
}
componentWillUnmount() {
PubSub.unsubscribe(this.token) //组件卸载前,取消订阅消息
}
render() {
return (
<div>
</div>
)
}
}
14.请求
- fetch https://github.com/camsong/blog/issues/2
fetch(`/api/change/name`).then(response => { //第一个response仅能获取到连接是否成功,拿不到数据
console.log('连接服务器成功');
return response.json() //第一个response的json方法可以继续返回一个Promise对象,该对象里包含了数据
}).then(response => {
console.log('获取数据', response) //从第二个response中就可以直接获取到数据
}).catch(error => {
console.log('请求错误', error.message)
})
//优化上面的代码
try {
const response = await fetch(`/api/change/name`)
const data = await response.json()
console.log('获取数据', data)
} catch(error) {
console.log('请求错误', error.message)
}
- axios
//略
15.路由
前端路由本质是靠操作window.history实现的,原理代码如下
<body>
<a href="http://www.atguigu.com" onclick="return push('/test1') ">push test1</a><br><br>
<button onClick="push('/test2')">push test2</button><br><br>
<button onClick="replace('/test3')">replace test3</button><br><br>
<button onClick="back()"><= 回退</button>
<button onClick="forword()">前进 =></button>
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
<script type="text/javascript">
// let history = History.createBrowserHistory() //方法一,直接使用H5推出的history身上的API
let history = History.createHashHistory() //方法二,hash值(锚点)后边都带一个#,兼容性极好
function push (path) {//入栈,浏览器路由跳转后默认执行的方法
history.push(path)
return false//改变浏览器的历史记录,但是不会进行跳转
}
function replace (path) {//替换栈顶记录,如果想禁用浏览器前进后退,就给顶层路由标签设置 replace={true}
history.replace(path)
}
function back() {//回退,出栈
history.goBack()
}
function forword() {//前进
history.goForward()
}
history.listen((location) => {//做监听,只要路径发生变化就执行
console.log('请求路由路径变化了', location)
})
</script>
</body>
</html>
react router针对web开发提供了react-router-dom
import { HashRouter, Route, Switch } from 'react-router-dom';
function App() {
return (
{/* 必须在外侧指定HashRouter或BrowserRouter标签 */}
<HashRouter>
{/* 使用Switch标签,如果存在相同path对应多个component,也只会匹配第1个 */}
<Switch>
{/* 注册路由,匹配路径展示路由,如果添加 exact={true} 则可以精准匹配路径,慎用 */}
<Route path="/" component={MainRoutes} />
</Switch>
</HashRouter>
);
}
路由组件和普通组件区别
- 路由组件放在pages文件夹中,普通组件放在components文件夹中
- 普通组件:<Login/>
- 路由组件:<Route path="/login" component={Login}/>
- 路由组件接收到的props包含了history、location、match三个对象,普通组件接收到的props属性只有父组件传递的值
- 通过 withRouter 可以使普通组件也拥有路由组件的这些属性
history:
go: f go(n) //n表示前进或后退步长
goBack: f goBack()
goForward: f goForward()
push: f push(path, state)
replace: f replace(path, state)
location:
pathname: "/login"
search: ""
state: undefined
match:
params: {} //从这里可以获取路由path匹配时传递的参数,如id,通过 this.props.match.params 获取
path: "/login"
url: "/login"
withRouter的使用
import {withRouter} from 'react-router-dom'
class Nomal extends Component {...}
export default withRouter(Header)
重定向:当遇到所有注册路由都无法匹配的路径,一般都设置重定向,卸载所有路由的最下方
import { Redirect, Switch, Route } from 'react-router-dom';
render() {
const redirectData = this.getRedirectData();
return (
<Switch>
{/* 渲染权限路由表 */}
{routerData.map(this.renderNormalRoute)}
{/* 路由重定向,嵌套路由默认重定向到当前菜单的第一个路由 */}
{redirectData.map((item, index) => {
return <Redirect key={index} exact from={item.from} to={item.to} />;
})}
{/* 首页默认重定向到 /login */}
<Redirect exact from="/" to="/login" />
{/* 未匹配到的路由重定向到 404 */}
<Route component={NotFound} />
</Switch>
);
}
}
路由传递参数
- params参数:能在地址栏显示参数
//向路由组件传递params参数
<Link to={`/user/${userData.id}`}>{userDada.username}</Link>
//路由中声明接收params参数
{
name: '用户信息',
path: '/user/:id',
layout: 'mainLayout',
component: UserInfo,
},
//组件中获取params参数
const { id } = this.props.params
- search参数:需要借助querystring解析
//向路由组件传递search参数
<Link to={`/user/?id=${userData.id}&username=${userDada.username}`}>{userDada.username}</Link>
//路由中声明接收search参数,正常写path,无需修改
{
name: '用户信息',
path: '/user',
layout: 'mainLayout',
component: UserInfo,
},
//组件获取search参数,首先 import qs from 'querystring'
const {search} = this.props.location
const {id, username} = qs.parse(search.slice(1))
- state参数:地址栏不显示参数,刷新地址,使用HashRouter会丢失参数,使用BrowserRouter不会丢失参数
//向路由组件传递state参数
<Link to={{
path: '/user',
state: {
id: userData.id,
username: userData.username
}
}}>{userDada.username}</Link>
//路由中声明接收state参数,正常写path,无需修改
{
name: '用户信息',
path: '/user',
layout: 'mainLayout',
component: UserInfo,
},
//组件获取state参数
const {id, username} = this.props.location.state
编程式路由跳转
pushRoute = (id, username) => {
this.props.history.push(`/user/${id}`)
}
<button onClick={() => this.pushRoute(userData.id, userData.username)}>手动push到新路由</button>
16.UI库
React国内常用ant-design作为UI库,由蚂蚁金服发布 https://ant.design/docs/react/introduce-cn
自定义主体颜色和按需引入样式的方法参考官方文档,或直接修改webpack配置文件,具体方法自行百度
17.Redux
- Store:存储数据的容器,全局一个
import { createStore } from 'redux';
//创建store
const store = createStore(fn);
//createStore可以传入3个参数
const store = createStore(
reducer, //参数1:所有的reducer集合
initial_state, //参数2(非必需):自定义state的初始状态
applyMiddleware(thunk, promise, logger) //参数3(非必需):中间件,如果不传initial_state,那么中间件就是第2个参数
);
//某一时间点中store存储的数据集合称为state,获取state
const state = store.getState();
//store可以监听state的变化
let unsubscribe = store.subscribe(listener); //一旦state发送变化就执行listener函数
unsubscribe(); //执行监听的返回结果函数就可以取消监听
- Action:组件中通过 store.dispatch(action) 通知Store
//Action对象包含 type 和 payload 两个属性
const action = {
type: 'actionname', //表示Action的名称,必需
payload: {} //表示Action携带的信息,非必需
}
//通过函数来返回action,这样的函数被称为 Action Creator
const addTodo = (data) => {
type: 'actionname'
data
}
const action = addTodo("LearnRedux")
//项目中,通常把一个面页中使用到的所有action函数都封装到一个actionCreator.js文件
- Reducer:Store接收到Action后通过Reducer修改state为新值并返回给Store
//reducer是函数,接收 旧的state 和 action 作为参数,返回 新state
const reducer = (state, action) => {
...
return newState;
}
//reducer函数不需要手动调用,而是由store.dispatch自动触发,每当store接收到action都会自动执行reducer
//要让store控制reducer,就需要在createStore时传入reducer
const store = createStore(reducer)
reducers的拆分
//Redux提供了一个 combineReducers 方法,用于Reducer的拆分
import { combineReducers } from 'redux';
const reducer = combineReducers({
//key为子组件名,value为对应reducer
chatLog: chatLogReducer //reducer 1
statusMessage: statusMessageReducer //reducer 2
userName: userNameReducer //reducer 3
...
})
export default reducer;
18.Redux中间件
详细教程参考:http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
中间件是一个函数,原理是对store.dispatch
方法进行改造,在 发出Action 和 执行Reducer 这两步之间,添加了其他功能
如何使用中间件:Redux提供了 applyMiddleware 函数
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import reducers from './reducers'
const store = createStore(
//封装的reducer函数集合
reducers,
//组合redux-thunk中间件和Redux DevTools浏览器插件
compose(applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__ ?window.__REDUX_DEVTOOLS_EXTENSION__() : f => f)
)
export default store;
redux-thunk
//redux-thunk可以改造store.dispatch(),使其可以接受函数作为参数,而默认只能接受对象作为参数
connect()
connect方法包装组件,使 组件 和 Store 连接起来
import { connect } from 'react-redux'
export default connect(
mapStateToProps, //映射state到props,返回一个对象,里面每一个键值对都是一个映射
mapDispatchToProps //映射action到props,返回一个对象
)(TodoList);
//mapStateToProps函数接收state作为参数,每当state更新的时候,就会自动执行
const mapStateToProps = (state) => {
return {
contact: state.getIn(['contact', 'contact']).toJS(),
};
};
//mapDispatchToProps函数接收dispatch作为参数
const mapDispatchToProps = (dispatch) => ({
getContact(keyword) {
dispatch(actionCreators.getContactOperation(keyword));
},
getContactDetail(id) {
dispatch(actionCreators.getContactDetailOperation(id));
},
deleteContact(id, value) {
dispatch(actionCreators.deleteContactOperation(id, value));
},
});