持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
React应用篇
基于React脚手架
使用create-react-app: 创建react应用
1. react脚手架
- xxx脚手架: 用来帮助程序员快速创建一个基于xxx库的模板项目
- 包含了所有需要的配置(语法检查、jsx编译、devServer…)
- 下载好了所有相关的依赖
- 可以直接运行一个简单效果
- 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库的支持)
- 导包规范:第三方的写在上面,自己的写在下面,样式写作最后
了解:
<React.StrictMode>包裹作用:检测App组件和App子组件的样式是否合理,用于检测组件中的错误
<React.StrictMode>
<App />
</React.StrictMode>
- {Component}:这里不是解构赋值
注意: 这里{Component}不是解构赋值出来的,是分别暴露出来的
css样式冲突
- 为了避免这样的css样式冲突,需要做样式模块化
css文件改为:Hello.module.css
// import './Hell.css' 改为:
import hello from './Hello.module.css'
{/* <h2 className="title">Hello React!</h2> */} 改为:
<h2 className={hello.title}>Hello React!<
/h2>
:golbal一般用于定义的类名不允许修改的情况,需要写上父类的类名,例 .aa
css module 配合sass (最常用)
- 创建xxx.module.scss
- 将父元素使用被module转换了的类名
import style from './index.moudule.scss'
<div className="style.father"></div>
- 子元素用被 global 包裹的全局属性
.layout{
// 写layout的样式
:global{
// 子元素的样式
}
}
4. 功能界面的组件化编码流程(通用)
- 拆分组件: 拆分界面,抽取组件
- 实现静态组件: 使用组件实现静态页面效果
- 实现动态组件
-
动态显示初始化数据
- 数据类型
- 数据名称
- 保存在哪个组件?
- 交互(从绑定事件监听开始)
5. TodoList 案例(需要掌握):
- 安装插件:快速编码
- 获取全球唯一的id:
npm i nanoid
- 效果图:
- 案例结构图:
- 案例总结:
主要步骤:
1.将状态存入父亲App的state中,然后通过props传递给List子节点
2.在list中,拿到todos通过map遍历,动态生成Item组件,再将数据传给每一个IItem
3.在Item中,通过拿到的数据中的name,done来渲染页面中的
4.将父亲App的state,通过props传递给Header子节点,通过输入框输入name,并通过addTodo函数返回给父节点App
5.点击Item,并返回该Item的id,和done,通过updateTodo函数传递给App,进行判断并跟新done
6.点击Item,并返回该Item的id,通过deleteTodo函数筛选出来与Item传过来的id不相同的newTodos,并存入状态重新渲染
7.在Item中给state中定义一个mouse=false,通过鼠标移入,移出事件,切换mouse状态,来改变背景
8.在Footer中,拿到App中的state,通过reduce或map来遍历里面的done,来确定已完成的个数,通过state的长度来判断总数
9.在Footer中,通过handleCheckAll函数,拿到全选按钮的checked布尔型,传给App跟新todos的done
10.在Footer中,通过已完成和全部的个数,来判断全选按钮的checked属性,为true或者false
11.在Footer中,点击清除完成任务按钮,调用App传来的claerAllDone函数,筛选出done=false的newObj,再重新存入状态重新
12.每个子节点对接收的props进行:类型,必要的限制
注意点:
defaultChecked={done}
:表示能够动态勾选
但是存在bug,它以一第一次勾选为准
<input type="checkbox" defaultChecked={done} />
- 兄弟传递数据的时候:这里是this.props不是this.state
const { todos, updateTodo, deleteTodo } = this.props
第4章:React ajax
1. 常用的ajax请求库
-
jQuery: 比较重, 如果需要另外引入不建议使用
-
axios: 轻量级, 建议使用
-
封装XmlHttpRequest对象的ajax
-
promise风格
-
可以用在浏览器端和node服务器端
-
2. axios
- GET请求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response.data);
})
.catch(function (error) {
console.log(error);
});
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
- POST请求
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
直接使用axios发起GET请求
axios({
method:'get',
url:'https://api.github.com/search/users?q=xxxxxx',
}).then(
response => {console.log('成功了',response.date);},
error => {console.log("失败了",error);}
)
3. 配置中间代理
方法一
在package.json中追加如下配置
"proxy":"http://localhost:5000"
说明:
- 优点:配置简单,前端请求资源时可以不加任何前缀。
- 缺点:不能配置多个代理。
- 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)
方法二
-
第一步:创建代理配置文件
在src下创建配置文件:src/setupProxy.js
-
编写setupProxy.js配置具体代理规则:
//低版本 //const proxy = require('http-proxy-middleware') //高版本 const { createProxyMiddleware: proxy } = require('http-proxy-middleware'); module.exports = function(app) { app.use( proxy('/api1', { //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000) target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址) changeOrigin: true, //控制服务器接收到的请求头中host字段的值 /* changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000 changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000 changeOrigin默认值为false,但我们一般将changeOrigin值设为true */ pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置) }), proxy('/api2', { target: 'http://localhost:5001', changeOrigin: true, pathRewrite: {'^/api2': ''} }) ) }
说明:
- 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
- 缺点:配置繁琐,前端请求资源时必须加前缀。
4. github搜索案例:
效果图:
总结:
1.设计状态时要考虑全面,例如带有网络请求的组件,要考虑请求失败怎么办。
2.ES6小知识点:解构赋值+重命名
let obj = {a:{b:1}}
const {a} = obj; //传统解构赋值
const {a:{b}} = obj; //连续解构赋值
const {a:{b:value}} = obj; //连续解构赋值+重命名
3.消息订阅与发布机制
1.先订阅,再发布(理解:有一种隔空对话的感觉)
2.适用于任意组件间通信
3.要在组件的componentWillUnmount中取消订阅
4.fetch发送请求(关注分离的设计思想)
try {
const response= await fetch(`/api1/search/users2?q=${keyWord}`)
const data = await response.json()
console.log(data);
} catch (error) {
console.log('请求出错',error);
}
步骤:
// ------不带订阅功能-------
// 1.通过Header中get获取到了用户信息,将其通过函数参数发送到App的state中
// 2.将App的state,发送给List中,进行数据渲染
// 3.完善
// 4.定义isfirst,islanding,error存入App状态,用于显示初始页面,加载完成,出错页面
// 4.1首先再Header中,在请求网络前,后,来定义这些属性的布尔型,再通过updateAppState函数传递给App
// 4.2然后将App所有状态传给List,用于显示初始页面,加载完成,出错页面
// ------带订阅功能(兄弟节点可以直接通信)-------
// 1.安装PubSubJS工具库
// 2.直接将state定义在List中,再订阅一下消息
// 3.在Header中,把通过网络请求接受到的消息,以发布消息的形式,发送给List
小技巧:连续结构赋值+重命名:
// 获取用户的输入(连续解构赋值+重命名)
const { keyWordElement: { value: keyWord } } = this
console.log(keyWord); // wh
注意:判断传过来到数据是数组,还是对象
// 拿到用户信息,传入saveUser函数
// 注意:这里传的是items用户信息,不是response.data
this.props.saveUser(response.data.items)
可以这样将App状态中的数据,批量传给List的props中
<List {...this.state} />
优化
- 消息订阅-发布机制
通过PubSubJS工具,来实现兄弟之间的通信
List中:
// 挂载完成,订阅消息,使用
componentDidMount() {
this.token = PubSub.subscribe('wh', (_, userObj) => {
this.setState(userObj)
console.log(this);
})
}
// 将要卸载,用完取消订阅
componentWillUnmount() {
PubSub.unsubscribe(this.token)
}
Header中:
PubSub.publish('wh', { islanding: false, users: response.data.items })
- 通过fetch发送网络请求:
// 发送网络请求,使用fetch发送(优化)
try {
const response = await fetch(`/api1/search/users?q=${keyWord}`)
const data = await response.json()
console.log(data);
PubSub.publish('wh', { islanding: false, users: data.items })
} catch (error) {
console.log('请求出错了', error);
PubSub.publish('wh', { islanding: false, error: error.message })
}
// 注:这段代码如果想运行,外面需要包一个 async function
5.扩展:Fetch(了解)
文档:
特点:
- fetch: 原生函数,不再使用XmlHttpRequest(xhr)对象提交ajax请求
- 老版本浏览器可能不支持
相关API:
- GET请求
fetch(url).then(function(response) {
return response.json()
}).then(function(data) {
console.log(data)
}).catch(function(e) {
console.log(e)
});
- POST请求
fetch(url, {
method: "POST",
body: JSON.stringify(data),
}).then(function(data) {
console.log(data)
}).catch(function(e) {
console.log(e)
})
第5章:React路由
1. SPA的理解
- 单页Web应用(single page web application,SPA)。
- 整个应用只有一个完整的页面。
- 点击页面中的链接不会刷新页面,只会做页面的局部更新。
- 数据都需要通过ajax请求获取, 并在前端异步展现。
2. 路由的理解
1. 什么是路由?
一个路由就是一个映射关系(key:value)
key为路径, value可能是function或component
2. 路由分类
后端路由:
-
理解: value是function, 用来处理客户端提交的请求。
-
注册路由: router.get(path, function(req, res))
-
工作过程:当node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
前端路由:
-
浏览器端路由,value是component,用于展示页面内容。
-
注册路由:
<Route path="/test" component={Test}>
-
工作过程:当浏览器的path变为/test时, 当前路由组件就会变为Test组件
3. react-router-dom的理解
react的一个插件库。
专门用来实现一个SPA应用。
基于react的项目基本都会用到此库。
4.前端路由基石:
效果图:
通过浏览器中的 history 来记录路径,replace 替换掉上一条路径(点击返回:到上上条路径),push(栈的方式)来最追加路径
<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
}
//replace替换掉上一条路径
function replace (path) {
history.replace(path)
}
function back() {
history.goBack()
}
function forword() {
history.goForward()
}
history.listen((location) => {
console.log('请求路由路径变化了', location)
})
</script>
5. react-router-dom相关API
1. <BrowserRouter>
<HashRouter>
<Route>
<Redirect>
<Link>
<NavLink>
<Switch>
2.其它
history对象
match对象
withRouter函数
路由的基本使用
1.明确好界面中的导航区、展示区
2.导航区的a标签改为Link标签
<Link to="/xxxxx">Demo</Link>
3.展示区写Route标签进行路径的匹配
<Route path='/xxxx' component={Demo}/>
4.<App>的最外侧包裹了一个<BrowserRouter>或<HashRouter>
下载:npm i react-router-dom@5.2.0
老版本(学习用)
App.jsx下:
import { Link, Route } from 'react-router-dom'
<div className="list-group">
{/* 原生html中,靠a标签跳转不同的页面 */}
{/* <a className="list-group-item" href="./about.html">About</a>
<a className="list-group-item active" href="./home.html">Home</a> */}
{/* 在React中靠路由链接实现切换组件 --编写路由*/}
{/* 一般用小写:/about */}
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
</div>
<div className="panel-body">
{/* 注册路由 */}
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
</div>
index.js下:
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
路由组件与一般组件
1.写法不同:
一般组件:<Demo/>
路由组件:<Route path="/demo" component={Demo}/>
2.存放位置不同:
一般组件:components
路由组件:pages
3.接收到的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"
NavLink与封装NavLink
- 给选中的标签加样式:用
NavLink
import { Route, NavLink } from 'react-router-dom'
<NavLink activeClassName="bgc" className="list-group-item" to="/home">Home</NavLink>
<NavLink activeClassName="bgc" className="list-group-item" to="/about">About</NavLink>
- 封装自己的
NavLink
:MyNavLink组件
components-MyNavLink-index.jsx下:
import React, { Component } from 'react'
import { NavLink } from 'react-router-dom'
export default class MyNavLink extends Component {
render() {
// console.log(this.props);
// {to: './about', children: 'About'}
return (
<NavLink activeClassName="bgc" className="list-group-item" {...this.props} />
)
}
}
使用:
<MyNavLink to="/home">Home</MyNavLink>
<MyNavLink to="/about">About</MyNavLink>
Switch的使用:
匹配到了/about后,就不会继续向下匹配
import { Route, NavLink, Switch } from 'react-router-dom'
<Switch>
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
<Route path="/about" component={Test} />
</Switch>
总结:
NavLink可以实现路由链接的高亮,通过activeClassName指定样式名
通常情况下,path和component是一一对应的关系。
Switch可以提高路由匹配效率(单一匹配)。
解决多级路径刷新页面样式丢失的问题:
1.public/index.html 中 引入样式时不写 ./ 写 / (常用)
2.public/index.html 中 引入样式时不写 ./ 写 %PUBLIC_URL% (常用)
3.使用HashRouter
路由的严格匹配与模糊匹配
1.默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
2.开启严格匹配:<Route exact={true} path="/about" component={About}/>
3.严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
Redirect的使用
1.一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由
2.具体编码:
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Redirect to="/about"/>
</Switch>
嵌套路由
1.注册子路由时要写上父路由的path值
2.路由的匹配是按照注册路由的顺序进行的
<Switch>
<Route path="/home/news" component={News} />
<Route path="/home/message" component={Message} />
<Redirect to="/home/news" />
</Switch>
向路由组件传递参数(重点)
总结:
1.params参数(常用)
路由链接(携带参数):<Link to='/demo/test/tom/18'}>详情</Link>
注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>
接收参数:this.props.match.params
优势 : 刷新地址栏,参数依然存在 缺点:只能传字符串,并且,如果传的值太多的话,url会变得长而丑陋
2.search参数(其次)
路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'}>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
接收参数:this.props.location.search
备注:获取到的search是urlencoded编码字符串,需要借助querystring解析
3.state参数(一般)
路由链接(携带参数):<Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
接收参数:this.props.location.state
优势:刷新也可以保留住参数
Message下:编写路由
messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
{/* 1.params方式 */}
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
{/* 2.search方式 */}
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
{/* 3.state方式 */}
<Link to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
</li>
)
})
声明路由:
{/* 1.声明接收params参数 */}
<Route path="/home/message/detail/:id/:title" component={Detail} />
{/* 2.search参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail} />
{/* 3.state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail} />
Detail下:接收参数
// 1.接收params参数
const { id, title } = this.props.match.params
// 2.接收saerch参数
const { search } = this.props.location
// search = '?id=01&title=消息1'
// console.log(search.slice(1)); //id=01&title=消息1
const { id, title } = qs.parse(search.slice(1))
// 3.接收state参数
const { id, title } = this.props.location.state || {}
//4.筛选结果
const findResult = DetailData.find((obj) => {
return obj.id === id
}) || {}
编程式路由导航
说明:点击按钮,实现push,replace,go.....跳转,主要操作的是history下的这些方法
Message下:
<li key={msgObj.id}>
{/* 1.params方式 */}
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
{/* 4.编程式路由导航 */}
<button onClick={() => { this.pushShow(msgObj.id, msgObj.title) }>push查看</button>
<button onClick={() => { this.replaceShow(msgObj.id, msgObj.title) }>replace查看</button>
</li>
{/* 1.声明接收params参数 */}
<Route path="/home/message/detail/:id/:title" component={Detail} />
{/* 4.编程式路由导航 */}
<button onClick={this.back}>回退</button>
<button onClick={this.forward}>前进</button>
<button onClick={this.go}>go</button>
replaceShow = (id, title) => {
// 编写一段代码,让其实现跳转到Detail组件,且为replace跳转
this.props.history.replace(`/home/message/detail/${id}/${title}`)
// 编写一段代码,让其实现跳转到Detail组件,且为search跳转
// this.props.history.replace(`/home/message/detail/?id=${id}&title=${title}`)
// 编写一段代码,让其实现跳转到Detail组件,且为state跳转
// this.props.history.replace('/home/message/detail', { id, title })
}
pushShow = (id, title) => {
// 编写一段代码,让其实现跳转到Detail组件,且为replace跳转
this.props.history.push(`/home/message/detail/${id}/${title}`)
// 编写一段代码,让其实现跳转到Detail组件,且为search跳转
// this.props.history.push(`/home/message/detail/?id=${id}&title=${title}`)
// 编写一段代码,让其实现跳转到Detail组件,且为state跳转
// this.props.history.push('/home/message/detail', { id, title })
}
back = () => {
this.props.history.goBack()
}
forward = () => {
this.props.history.goForward()
}
go = () => {
this.props.history.go(2)
}
或设置自动跳转:
在News下:
componentDidMount() {
setInterval(() => {
this.props.history.push(`/home/message/detail/`)
}, 2000)
}
withRouter
withRouter可以加工一般组件,让一般组件具备路由组件所特有的API
withRouter的返回值是一个新组件
import { withRouter } from 'react-router-dom'
export default withRouter(Home)
BrowserRouter与HashRouter的区别
1.底层原理不一样:
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值。
2.path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test
3.刷新后对路由state参数的影响
(1).BrowserRouter没有任何影响,因为state保存在history对象中。
(2).HashRouter刷新后会导致路由state参数的丢失!!!
4.备注:HashRouter可以用于解决一些路径错误相关的问题。
第6章:React UI组件库
1. material-ui(国外)
github: github.com/callemall/m…
2. ant-design(国内蚂蚁金服)
Github: github.com/ant-design/…
3. antd的按需引入+自定主题
1.安装依赖:yarn add react-app-rewired customize-cra babel-plugin-import less less-loader
2.修改package.json
....
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
....
3.根目录下创建config-overrides.js
//配置具体的修改规则
const { override, fixBabelImports,addLessLoader} = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
lessOptions:{
javascriptEnabled: true,
modifyVars: { '@primary-color': 'green' },
}
}),
);
4.备注:不用在组件里亲自引入样式了,即:import 'antd/dist/antd.css'应该删掉
error:
WARNING in ./node_modules/antd/dist/antd.css (./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[1].oneOf[5].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[1].oneOf[5].use[2]!./node_modules/source-map-loader/dist/cjs.js!./node_modules/antd/dist/antd.css)
Compiled successfully!
出现这个问题是因为 react-script 升级到5.0.0,通过npx create-react-app 创建的项目,引入antd.css 之后会看到这个警告。 解决方法就是 将 import 'antd/dist/antd.css'; 替换为 import 'antd/dist/antd.min.css';