3、React应用(基于React脚手架)
1)、使用create-react-app创建react应用
1)react脚手架
react提供了一个用于创建react项目的脚手架库:create-react-app
项目的整体技术架构为:react+webpack+es6+eslint
2)创建项目并启动
第一步,全局安装:npm i -g create-react-app
第二步,切换到想创项目的目录,使用命令:create-react-app hello-react
第三步,进入项目文件夹:cd hello-react
第四步,启动项目:npm start
3)react脚手架项目结构
public——静态资源文件夹
favicon.icon——网站页签图标
index.html——主页面
logo192.png——logo图
logo512.png——logo图
manifest.json——应用加壳的配置文件
robots.txt——爬虫协议文件
src——源码文件夹
App.css——App组件的样式
App.js——App组件
App.test.js——用于给App做测试
index.css——样式
index.js——入口文件
logo.svg——logo图
reportWebVitals.js——页面性能分析文件(需要web-vitals库的支持)
setupTests.js——组件单元测试的文件(需要jest-dom库的支持)
2)、todoList案例相关知识点
1)拆分组件、实现静态组件,注意:className、style的写法
2)动态初始化列表,如何确定将数据放在哪个组件的state中?
- 某个组件使用:放在其自身的state中
- 某些组件使用:放在他们共同的父组件state中(官方称此操作为:状态提升)
3)关于父子之间通信:
- 【父组件】给【子组件】传递数据:通过props传递
- 【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
父组件:
import React, { Component } from 'react'
import Header from './components/Header/index'
import List from './components/List/index'
import Footer from './components/Footer/index'
import './App.css'
//创建并暴露App组件
export default class App extends Component {
//初始化状态
state = {
todos: [
{ id: '001', name: '吃饭', done: true },
{ id: '002', name: '睡觉', done: true },
{ id: '003', name: '打代码', done: false }
]
}
addTodo = (todoObj) => {
const { todos } = this.state
const newTodos = [todoObj, ...todos]
this.setState({ todos: newTodos })
}
updateTodo = (id, done) => {
const { todos } = this.state
const newTodos = todos.map(todoObj => {
if (todoObj.id === id) {
return { ...todoObj, done: done }
} else {
return todoObj
}
})
this.setState({ todos: newTodos })
}
deleteTodo = (id) => {
const { todos } = this.state
const newTodos = todos.filter((todoObj) => {
return todoObj.id !== id
})
this.setState({ todos: newTodos })
}
checkAllTodo = (done) => {
const { todos } = this.state
const newTodos = todos.map((todoObj) => {
return { ...todoObj, done: done }
})
this.setState({ todos: newTodos })
}
clearAllDone = () => {
const { todos } = this.state
const newTodos = todos.filter((todoObj) => {
return !todoObj.done
})
this.setState({ todos: newTodos })
}
render() {
const { todos } = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo} />
<List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo} />
<Footer todos={todos} checkAllTodo={this.checkAllTodo} clearAllDone={this.clearAllDone} />
</div>
</div>
);
}
}
Header子组件:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { nanoid } from 'nanoid'
import './index.css'
export default class Header extends Component {
static propTypes = {
addTodo: PropTypes.func.isRequired
}
handleKeyUp = (event) => {
const { target, keyCode } = event
const { addTodo } = this.props
if (keyCode !== 13) {
return
}
if (target.value.trim() === '') {
alert('输入不能为空')
return
}
const todoObj = { id: nanoid(), name: target.value, done: false }
addTodo(todoObj)
target.value = ''
}
render() {
return (
<div className="todo-header">
<input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认" />
</div>
)
}
}
List子组件:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Item from '../Item/index'
import './index.css'
export default class List extends Component {
static propTypes = {
todos: PropTypes.array.isRequired,
updateTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired
}
render() {
const { todos, updateTodo, deleteTodo } = this.props
return (
<ul className="todo-main">
{
todos.map(todo => {
return <Item key={todo.id} {...todo} updateTodo={updateTodo} deleteTodo={deleteTodo} />
})
}
</ul>
)
}
}
Item子组件:
import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
state = {
mouse: false
}
handleMouse = (flag) => {
return () => {
this.setState({ mouse: flag })
}
}
handleCheck = (id) => {
const { updateTodo } = this.props
return (event) => {
updateTodo(id, event.target.checked)
}
}
handleDelete = (id) => {
const { deleteTodo } = this.props
if (window.confirm('确定删除吗?')) {
deleteTodo(id)
}
}
render() {
const { id, name, done } = this.props
const { mouse } = this.state
return (
<li style={{ backgroundColor: mouse ? '#ddd' : '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)} className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }}>删除</button>
</li>
)
}
}
Footer子组件:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './index.css'
export default class Footer extends Component {
static propTypes = {
todos: PropTypes.array.isRequired,
checkAllTodo: PropTypes.func.isRequired,
clearAllDone: PropTypes.func.isRequired
}
handleCheckAll = (event) => {
const { checkAllTodo } = this.props
checkAllTodo(event.target.checked)
}
handleClearAllDone = () => {
const { clearAllDone } = this.props
clearAllDone()
}
render() {
const { todos } = this.props
const doneCount = todos.reduce((pre, todo) => { return pre + (todo.done ? 1 : 0) }, 0)
const total = todos.length
return (
<div className="todo-footer">
<label>
<input type="checkbox" onChange={this.handleCheckAll} checked={total !== 0 && doneCount === total} />
</label>
<span>
<span>已完成{doneCount}</span> / 全部{total}
</span>
<button onClick={this.handleClearAllDone} className="btn btn-danger">清除已完成任务</button>
</div>
)
}
}
4)注意defaultChecked和checked的区别,类似的还有:defaultValue和value
5)状态在哪里,操作状态的方法就在哪里
4、React路由
1)、相关理解
1)SPA的理解
- 单页Web应用(single page web application,SPA)
- 整个应用只有一个完整的页面
- 点击页面中的链接不会刷新页面,只会做页面的局部更新
- 数据都需要通过ajax请求获取,并在前端异步展现
2)路由的理解
什么是路由?
- 一个路由就是一个映射关系(key:value)
- key为路径,value可能是function或component
路由分类:
a.后端路由:
理解:value是function,用来处理客户端提交的请求
注册路由:router.get(path, function(req, res))
工作过程:当node接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据
b.前端路由:
浏览器端路由,value是component,用于展示页面内容
注册路由:<Route path="/test" component={Test}>
工作过程:当浏览器的path变为/test时,当前路由组件就会变为Test组件
2)、基本路由使用
1)引入react-router-dom
yarn add react-router-dom
2)路由的基本使用
- 明确好界面中的导航区、展示区
- 导航区的a标签改为Link标签
<Link to="/xxxxx">Demo</Link>
- 展示区写Route标签进行路径的匹配
<Route path='/xxxx' component={Demo}/>
<App>
的最外侧包裹了一个<BrowserRouter>
或<HashRouter>
3)代码实现
App.jsx:
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import Home from './pages/Home/index'
import About from './pages/About/index'
export default class App extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 编写路由链接 */}
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
</div>
</div>
</div>
</div>
</div>
)
}
}
index.js:
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
ReactDOM.render(
<BrowserRouter><App /></BrowserRouter>,
document.getElementById('root')
)
3)、路由组件与一般组件
1)写法不同
一般组件:<Demo/>
路由组件:<Route path="/demo" component={Demo}/>
2)存放位置不同
一般组件:components
路由组件:pages
接收到的props不同:
一般组件:写组件标签时传递了什么,就能收到什么
路由组件:接收到三个固定的属性
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/about"
search: ""
state: undefined
match:
params: {}
path: "/about"
url: "/about"
4)、NavLink与封装NavLink
- NavLink可以实现路由链接的高亮,通过activeClassName指定样式名
- 标签体内容是一个特殊的标签属性
- 通过this.props.children可以获取标签体内容
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { NavLink } from 'react-router-dom'
import './index.css'
export default class MyNavLink extends Component {
static propTypes = {
to: PropTypes.string.isRequired,
children: PropTypes.string.isRequired
}
render() {
return (
<NavLink activeClassName="demo" className="list-group-item" {...this.props} />
)
}
}
App.jsx:
import React, { Component } from 'react'
import { Route } from 'react-router-dom'
import MyNavLink from './components/MyNavLink/index'
import Home from './pages/Home/index'
import About from './pages/About/index'
export default class App extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 编写路由链接 */}
<MyNavLink to="/home">Home</MyNavLink>
<MyNavLink to="/about">About</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
</div>
</div>
</div>
</div>
</div>
)
}
}
5)、Switch的使用
- 通常情况下,path和component是一一对应的关系
- Switch可以提高路由匹配效率(单一匹配)
App.jsx:
import React, { Component } from 'react'
import { Route, Switch } from 'react-router-dom'
import MyNavLink from './components/MyNavLink/index'
import Home from './pages/Home/index'
import About from './pages/About/index'
export default class App extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 编写路由链接 */}
<MyNavLink to="/home">Home</MyNavLink>
<MyNavLink to="/about">About</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Switch>
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
</Switch>
</div>
</div>
</div>
</div>
</div>
)
}
}
6)、路由的严格匹配与模糊匹配
- 默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
- 开启严格匹配:
<Route exact={true} path="/about" component={About}/>
- 严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
7)、Redirect的使用
一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由
<Switch>
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
<Redirect to="/about" />
</Switch>
8)、嵌套路由
- 注册子路由时要写上父路由的path值
- 路由的匹配是按照注册路由的顺序进行的
9)、向路由组件传递参数
1)params参数
路由链接(携带参数):
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
注册路由(声明接收):
<Route path="/home/message/detail/:id/:title" component={Detail} />
接收参数:this.props.match.params
父组件:
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import Detail from './Detail/index'
export default class Message extends Component {
state = {
messageArr: [
{ id: '01', title: '消息1' },
{ id: '02', title: '消息2' },
{ id: '03', title: '消息3' }
]
}
render() {
const { messageArr } = this.state
return (
<div>
<ul>
{
messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
{/* 向路由组件传递params参数 */}
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr />
{/* 声明接收params参数 */}
<Route path="/home/message/detail/:id/:title" component={Detail} />
</div>
)
}
}
子组件:
import React, { Component } from 'react'
const detailData = [
{ id: '01', content: '消息1详情' },
{ id: '02', content: '消息2详情' },
{ id: '03', content: '消息3详情' }
]
export default class Detail extends Component {
render() {
//接收params参数
const { id, title } = this.props.match.params
const findResult = detailData.find((detailObj) => {
return detailObj.id === id
})
return (
<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>
)
}
}
2)search参数
路由链接(携带参数):
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
注册路由(无需声明,正常注册即可):
<Route path="/home/message/detail/" component={Detail} />
接收参数:this.props.location.search
备注:获取到的search是urlencoded编码字符串,需要借助querystring解析
父组件:
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import Detail from './Detail/index'
export default class Message extends Component {
state = {
messageArr: [
{ id: '01', title: '消息1' },
{ id: '02', title: '消息2' },
{ id: '03', title: '消息3' }
]
}
render() {
const { messageArr } = this.state
return (
<div>
<ul>
{
messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
{/* 向路由组件传递search参数 */}
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr />
{/* search参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail/" component={Detail} />
</div>
)
}
}
子组件:
import React, { Component } from 'react'
import qs from 'querystring'
const detailData = [
{ id: '01', content: '消息1详情' },
{ id: '02', content: '消息2详情' },
{ id: '03', content: '消息3详情' }
]
export default class Detail extends Component {
render() {
//接收search参数
const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1))
const findResult = detailData.find((detailObj) => {
return detailObj.id === id
})
return (
<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>
)
}
}
3)state参数
路由链接(携带参数):
<Link to={{ pathname: '/home/message/detail/', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
注册路由(无需声明,正常注册即可):
<Route path="/home/message/detail/" component={Detail} />
接收参数:this.props.location.state
备注:刷新也可以保留住参数
父组件:
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import Detail from './Detail/index'
export default class Message extends Component {
state = {
messageArr: [
{ id: '01', title: '消息1' },
{ id: '02', title: '消息2' },
{ id: '03', title: '消息3' }
]
}
render() {
const { messageArr } = this.state
return (
<div>
<ul>
{
messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
{/* 向路由组件传递state参数 */}
<Link to={{ pathname: '/home/message/detail/', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr />
{/* state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail/" component={Detail} />
</div>
)
}
}
子组件:
import React, { Component } from 'react'
const detailData = [
{ id: '01', content: '消息1详情' },
{ id: '02', content: '消息2详情' },
{ id: '03', content: '消息3详情' }
]
export default class Detail extends Component {
render() {
//接收state参数
const { id, title } = this.props.location.state || {}
const findResult = detailData.find((detailObj) => {
return detailObj.id === id
}) || {}
return (
<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>
)
}
}
10)、编程式路由导航
借助this.prosp.history
对象上的API对操作路由跳转、前进、后退
this.prosp.history.push()
this.prosp.history.replace()
this.prosp.history.goBack()
this.prosp.history.goForward()
this.prosp.history.go()
11)、BrowserRouter与HashRouter的区别
-
底层原理不一样
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本
HashRouter使用的是URL的哈希值
-
path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test
-
刷新后对路由state参数的影响
BrowserRouter没有任何影响,因为state保存在history对象中
HashRouter刷新后会导致路由state参数的丢失
-
备注:HashRouter可以用于解决一些路径错误相关的问题
视频资料:
https://www.bilibili.com/video/BV1wy4y1D7JT
源码地址:
https://github.com/hxt970311/react_demo