目录
跳转链接 => React_01 学习笔记
跳转链接 => React_02 学习笔记
跳转链接 => React_04 菜谱项目
跳转链接 => React_05 Hooks
- 能够使用 redux 进行 数据状态管理
- 能够使用 redux-thunk 完成 异步请求
- 能够 redux 模块化 拆分
- 完成 编程式导航 跳转
- 掌握 3 种 路由渲染 方式
- 定义好 路由参数数据 和 获取
- 能够 自定义导航组件
- 能够把 iummutable 集成到 redux 中
一、状态管理 (redux)
1.1、简介
2013年 Facebook 提出了 Flux 架构的思想,引发了很多的实现。2015年,Redux 出现,将 Flux 与 函数式编程 结合一起,很短时间内就成为了最热门的 前端架构。
简单说,如果你的 UI 层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。如果你的项目组件的数量和层级也变得越来越多,越来越深,此时组件间的 数据通信 就变得异常的 复杂和低效,为了解决这个问题,引入了 状态管理( redux ) 从而很好的 解决多组件之间的 通信问题。
1.2、安装 Redux
redux 不是内嵌在 react 框架中,使用时需要手动去 安装
npm i -S redux "redux": "^4.1.1",
1.3、三大原则
- 单一 数据源
整个应用的 state 被储存在一棵对象结构中,并且这个 对象结构只存在于 唯一 一个 store 中
- State 是 只读 的
redux 中的 state 只读 的 不可以直接修改
- 使用纯函数 ( reducer ) 来执行修改 state
为了修改了 state 数据,redux 定义了一个 reducer 函数来完成 state 数据的 修改,reducer 会接收先前的 state 和 action,并返回 新的 state
操作原理图 :
①、store ( 存储 ) 通过 reducer 创建了 初始状态
②、component ( 组件 ) 通过 store.getState( ) 方法获取 得到了 store 中保存的 state 数据 挂载在了自己的状态上
③、用户产生了操作,调用了 actions 的方法
④、actions 的方法被调用,创建了带有标示性信息的 action(描述对象)
⑤、actions 将 action 通过 调用 store.dispatch 方法发送到了 reducer 中
⑥、reducer 接收到 action 并根据标识信息判断之后返回了 新的 state
⑦、store 的 state 被 reducer 更改为新 state 的时候,store.subscribe 方法里的 回调函数 会执行,此时就可以通知 component 去 重新获取 state
1.4、使用 redux
计数器 案例:
- 创建统一的 数据源
在 src 目录下新建 store / index.js 作为 数据源 声明文件
// 常规导入
import { createStore } from "redux"
// 创建默认的 数据源(state)
const defaultState = {
// 初始数据
count: 0
}
// 负责处理数据
function reducers(state = defaultState, action) {
// 返回新的 state 数据
return state
}
// 创建仓库
const store = createStore(reducers)
// 导出
export default store
- 组件中获取 / 设置数据
import React, { Component } from "react"
// 导入仓库
import store from './store'
class App extends Component {
constructor(props){
super(props)
// 获取初始数据
this.state = store.getState()
}
componentDidMount(){
// 订阅数据(获取更新)
store.subscribe(() => this.setState(state => store.getState()))
}
render() {
return (
<div>
{ /* 渲染的内容 */ }
</div>
);
}
incr() {
// 任务清单
const actionCreator = {
type: 'incr',
payload: 1
}
// 派发任务清单
store.dispatch(actionCreator);
}
}
export default App;
- 安装 Redux DevTools
为了方便调试 redux(可选安装),建议去谷歌商店安装 redux-devtools
在使用的时候需要参考其说明页面,参考其使用说明给代码做对应的调整。
// 创建仓库
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
随后打开浏览器调试工具就可以看到类似如下界面:
1.5、代码模块化
现在我们已经能够很好的进行 redux 的 数据管理,但是有一个缺点就是所有的代码都写在一个文件中,需要按照 模块化开发 的规则 进行对 代码 拆分。
未拆分前的代码 :
index.js
// redux 入口 , 仓库
// 常规导入
import { createStore } from 'redux'
// 创建默认的数据源 state
const initState = {
// 初始数据
count: 100
}
const mutation = {
incr(state, data) {
return {
...state,
count: state.count + data
}
}
}
function reducers(state = initState, action) {
console.log(action.type);
try {
return mutation[action.type](state, action.payload)
} catch (error) {
return state
}
}
// 负责处理 state 状态的函数 , 这个函数一定要有返回值
/* function reducers(state = initState, action) {
if ('incr' === action.type) {
// 深复制 , 返回新的 state 对象
// return {
// ...state,
// count: state.count + action.payload
// }
let newState = JSON.parse(JSON.stringify(state))
newState.count = newState.count + action.payload
return newState
}
// 返回新的 state 数据
return state
} */
// 创建仓库
const store = createStore(
reducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
// 导出
export default store
App.jsx
import React, { Component } from 'react';
// 有一个 store 对象
import store from './store';
class App extends Component {
// 私有状态
state = store.getState()
addNum = () => {
// 修改 redux 中的 state 数据对象
let actionCreate = {
type: 'incr',
payload: 10
}
// 组件通过 redux 来完成对于 state 数据的更新 -- 更新是同步操作
store.dispatch(actionCreate)
// 手动获取一次最新的数据
this.setState(store.getState)
}
// 挂载完毕后 , 主动订阅 redux
componentDidMount() {
// 订阅
// debugger
this.unsubscribe = store.subscribe(() => this.setState(store.getState()))
}
componentWillUnmount() {
// 取消订阅
this.unsubscribe()
}
render() {
return (
<div>
<h3>{this.state.count}</h3>
<button onClick={this.addNum}>+++</button>
</div>
);
}
}
export default App;
模块化拆分后的代码 :
拆分 index.js
拆分 App.jsx
二、react-redux
React-Redux 是 Redux 的官方针对 React 开发的 扩展库,默认没有在 React 项目中安装,需要手动来安装。react-redux 是依赖于 redux ,所以你必须安装 redux
你可以理解为 react-redux 就是 redux 给我们提供一些 高阶组件 ,能解决的问题是:使用它以后我们不需要在每个组件中再去手动 订阅数据的更新了。
安装指令 : npm install -D redux-devtools-extension
// 让 redux 在 chrome( 谷歌浏览器 ) 中的 redux 插件中能 调试
import { composeWithDevTools } from 'redux-devtools-extension';
import { createStore } from 'redux' // 让redux在chrome中redux插件中能调试 import { composeWithDevTools } from 'redux-devtools-extension'; // 初始值 const initState = { count: 100, films: [] } // 操作 state 方法 const mutation = { incr(state, data) { return { ...state, count: state.count + data } } } const reducer = (state = initState, { type, data }) => { try { return mutation[type](state, data) } catch (error) { return state } } export default createStore( reducer, composeWithDevTools() )
npm i -S react-redux redux
- 定义 Provider
在程序主文件 index.js 文件中, 定义 Provider
此处类似于之前 跨组件通信处的 Provider 一样,旨在让全局的组件 共享 store 中的数据
- 在 子组件 中使用 react-redux
react-redux 拆分 :
三、redux 中间件
3.1、介绍
redux 默认支持 同步操作,异步操作 怎么办?Action 发出以后,Reducer 立即算出 State ,这叫做同步;Action 发出以后,过一段时间再执行 Reducer ,这就是 异步 。怎么才能 Reducer 在 异步操作 结束后自动执行呢?这就要用到新的工具:中间件 。中间件有很多,这里使用一个 Redux 官方出品的 中间件库:redux-thunk
3.2、使用 redux-thunk
它支持函数返回,本质还是在 在内部调用 dispatch 返回一个固定值 对象
npm i -S redux-thunk
在 createStore 实例 store 中使用
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
const store = createStore(
reducer,
applyMiddleware(thunk)
)
export default store
注:需要注意,加上了以上代码后,redux 的调试工具就会失效并且会报错,如果还想使用 redux 的调试工具,则需要调整代码为
const composeEnhancers = (typeof window !== "undefined" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
const store = createStore(
reducer,
composeEnhancers(applyMiddleware(thunk))
);
# action
export const incrCount = () => dispatch => {
setTimeout(() => {
dispatch({
type: 'add',
payload: 1
})
}, 1000)
}
四、Redux 模块化
Redux 提供了一个 combineReducers 方法,用于 Reducer 的拆分。你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer。
ESLint 报的错误 , 只是告诉你在赋值之前最好不要直接导入
// eslint-disable-next-line
自动化导入 reducer :
// 参数1 : 扫描的目录
// 参数2 : 是否递归
// 参数3 : 返回什么样扩展名的文件
const moduleFn = require.context('../reducers', false, /\.js$/)
const modules = moduleFn.keys().reduce((pv, cv) => {
let key = cv.match(/\.\/(\w+)\.js$/)[1]
let value = moduleFn(cv).default
pv[key] = value
return pv
}, {})
五、路由
5.1、介绍
现代的前端应用大多数是 SPA( 单页应用程序 ),也就是只有一个 HTML 页面的应用程序。因为它的 用户体验更好、对服务器压力更小,所以更受欢迎。为了有效的使用 单个页面 来管理 多页面的功能,前端路由 应运而生。
- 前端路由功能:让用户从一个视图 ( 组件 ) 导航到另一个视图 ( 组件 )
- 前端路由是一套 映射 规则,在 React 中,是 URL 路径 与 组件 的对应关系
- 使用 React 路由简单来说,就是配置 路径 和 组件
5.2、路由使用
React Router: Declarative Routing for React.js
5.2.1、安装路由模块
路由模块 不是 react 自带模块,需要安装第 3 方模块
npm i -S react-router-dom
5.2.2、相关组件
- Router 组件:包裹整个应用,一个 React 应用只需要使用一次
Router: HashRouter 和 BrowserRouter
HashRouter: 使用 URL 的 哈希值 实现 ( localhost:3000/#/first )
BrowserRouter:使用 H5 的 history API 实现( localhost:3000/first )
- Link / NavLink 组件:用于指定 导航链接( a 标签 )
最终 Link 会编译成 a 标签,而 to 属性会被编译成 a 标签的 href 属性
- Route 组件:指定路由 展示组件 相关信息( 组件渲染 )
path 属性:路由规则,这里需要跟 Link 组件里面 to 属性的值一致
component 属性:展示的 组件
各组件关系示意图
定义项目使用路由,在入口文件 / src / index.js 文件中定义 路由模式
定义 路由规则 和 匹配成功 的 渲染组件
在浏览器中输入后尝试 匹配
5.3、声明式导航
使用 Link 或 NavLink 组件完成 声明式导航 的 定义
Link / NavLink 区别
- Link 组件不会根据 路由 的 变化 而 添加或修改 编译后 html 标签中的 属性
- NavLink 会根据 路由 的 变化 而 自动修改 编译后 html 标签中的 属性
如果当前的 路由规则 和 Navlink 中的 To 所写的 规则一致 则 添加 class 样式,
默认名称为 active ,可以通过 activeClassName 来修改 匹配成功后 样式名称。
import React, { Component } from 'react'
import { HashRouter as Router, Route, Link } from 'react-router-dom'
import Home from './pages/Home'
import News from './pages/News'
class App extends Component {
render() {
return (
<Router>
<h3>导航区域</h3>
<hr />
<ul>
<li>
<Link to="/home">首页</Link>
</li>
<li>
<NavLink to="/news">新闻</NavLink>
</li>
</ul>
<hr />
<Route path="/home" component={Home} />
<Route path="/news" component={News} />
</Router>
);
}
}
export default App
5.4、编程式导航
react-router-dom 中通过 history 对象中的 push / replace / go 等方法 实现 编程式导航 功能。
注:默认在 react 组件中,只能通过 路由匹配 成功后 直接 渲染组件才会有 路由对象,只有有 路由对象 ,才能 实现 编程式导航 。
5.5、路由参数
路由参数:在 Route 定义 渲染组件 时给定动态绑定的参数。
React 路由传参方式有三种:
- 动态路由参数( param )
以 “/detail/:id” 形式传递的数据
在 落地组件 中通过 this.props.match.params 得到
- 查询字符串(search)
通过地址栏中的 ?key=value&key=value 传递
在落地组件中通过 this.props.location.search 得到
- 隐式传参(state),通过 地址栏 是观察不到的
通过 路由对象 中的 state 属性进行 数据传递
在落地组件中通过 this.props.location.state 得到
5.6、嵌套路由
在有一些功能中,往往 请求地址 的 前缀 是相同的,不同的只是后面一部份,此时就可以使用 多级路由( 路由嵌套 )来实现此路由的 定义实现 。
例如,路由规则 如下
admin / index
admin / user
它们路由前缀的 admin 是相同的,不同的只是后面一部份。
实现方式
- 先需要定义个组件,用于负责匹配 同一前缀 的 路由,将匹配到的路由指向到 具体的模块
<Route path="/admin" component={Admin}></Route>
- 创建模块路由组件负责指定各个路由的 去向
5.7、三种路由渲染方式
5.7.1、component (组件对象或函数)
5.7.2、render (函数)
<Route path="/home" render={router=><Home {…router} />} />
5.7.3、children (函数或组件)
5.8、withRouter 高阶组件
作用:把不是通过路由直接渲染出来的组件,将 react-router 的 history、location、match 三个对象传入 props 对象上
默认情况下必须是经过路由匹配渲染的组件才存在 this.props ,才拥有路由参数,才能使用 编程式导航 的写法,执行 this.props.history.push('/uri') 跳转到对应路由的页面,然而不是所有组件都直接与路由相连的,当这些组件需要 路由参数 时,使用 withRouter 就可以给此组件传入路由参数,此时就可以使用 this.props
5.9、自定义导航组件
为何需要 自定义导航 ?
因为在项目中往往不是所有的 声明式导航 都是需要 a 标签完成,有时候可能需要别的标签,此时如果在需要的地方去写 编程式导航 就会有代码重复可能性,就在对于公共代码进行提取。
思路:
- 定义一个 普通组件 可以是 类组件 也可以是 函数式组件
- 父组件 能向 子组件 传值 props
- 此 高阶组件 不管 路由规则 是否匹配都要有渲染 children
六、过渡动画组件
6.1、给组件使用过渡动画
npm i -S react-transition-group
6.2、列表过渡
import React, { Component } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import './app.css'
class App extends Component {
state = {
todos: []
}
render() {
return (
<div>
<TransitionGroup>
{/* 使用了 TransitionGroup,这样的列表过渡动画,
则 CSSTransition 中原本的 in属性不要用了,取而代之为 key, key唯一不重复的 */}
{
this.state.todos.map((item, index) => (
<CSSTransition
// 唯一的
key={item.id}
// 动画时长 毫秒
timeout={600}
// string|object 样式前缀名称|自定义样式名称
classNames="wu"
// 动画结束后删除对应的dom
unmountOnExit
>
<div>
<span>{index + 1}</span>
<span>{item.title}</span>
<span onClick={() => {
// 注意要用回调函数的方案,为了数据完整性
this.setState(state => {
// 当有获取原数据时,建议全用回调函数
let todos = state.todos
todos.splice(index, 1)
return {
todos
}
})
}}>删除</span>
</div>
</CSSTransition>
))
}
</TransitionGroup>
<button onClick={() => {
this.setState(state => ({
todos: [...state.todos, {
id: Date.now(),
title: 'abc-- ' + Date.now()
}]
}))
}}>添加任务</button>
</div>
);
}
}
export default App;
6.3、利用高阶组件给组件添加动画
并不想让所有的路由都有动画效果,只是想对指定的页面有路由切换效果,可以利用 高阶组件 来完成。让组件有动画
# 定义 高阶组件
import React, { Component } from 'react'
import { CSSTransition } from 'react-transition-group'
//import '../assets/animate.css'
const withAnimation = Cmp => {
return class extends Component {
render() {
return (
<CSSTransition
in={this.props.match !== null}
timeout={600}
classNames={{
enter: 'animate__animated',
enterActive: 'animate__fadeIn',
exit: 'animate__animated',
exitActive: 'animate__fadeOut'
}}
unmountOnExit
>
<Cmp {...this.props} />
</CSSTransition>
)
}
}
}
export default withAnimation
# 使用
@withAnimation
class Page extends Component {
render() {
return <div>高阶组件完成路由切换动画效果</div>
}
}
// 使用高阶组件定义路由动画组件是一定要用它的 Route children 来渲染组件
<Route path="/home" children={props => <Page {...props} />} />
6.4、集合 animate.css
网址:Animate.css | A cross-browser library of CSS animations.
npm install animate.css --save
七、immutable.js
7.1、概述
官网:Immutable.js
List (List) - Immutable 3.8.1 中文开发手册 - 开发者手册 - 云+社区 - 腾讯云
Immutable.js 出自 Facebook ,是最流行的 不可变数据结构 的实现之一。它实现了完全的持久化数据结构,使用结构共享。所有的更新操作都会返回新的值,但是在内部结构是共享的,来减少内存占用 ( 和垃圾回收的失效 )。
持久化数据结构:这里说的 持久化 是用来描述一种 数据结构,指一个数据,在被修改时,仍然能够保持 修改前的状态,即 不可变类型。
结构共享:Immutable 使用先进的 tries (字典树) 技术实现结构共享来解决性能问题,当我们对一个 Immutable 对象进行操作的时候,ImmutableJS 会只 clone 该节点以及它的祖先节点,其他保持不变,这样可以共享相同的部分,大大提高性能。
惰性操作:创建的对象时其实代码块没有被执行,只是被声明了,代码在获取或修改的时候才会实际被执行
7.2、使用 immutable 优缺点
7.3、安装
npm i -S immutable
7.4、常用 Api
fromJS => 递归 : 自动把数组 或 对象转为 Map 或 List
Map 是 immutable 中的 对象 ,不是原生 js 中的 对象
数据获取 get ( 单层级 获取 ) / getIn ( 多层级 获取数据 )
is => 专门用来 比对数据
数据修改 => set / setIn (深层修改) / update / updateIn (深层修改)
// set 设置值,不需要原值,update 可以保留原值 的基础上来进行修改
// toJS 方法,就是把 immutable 对象转为原生 js 对象
合并对象 = > // 如果两者有相同字段,则以合并中的方法字段为准
// 删除 istate.removeIn(['键名'])
List 类型 对应数组
// List 把 js 中的数组转为 List 它只能转换数组的第1层
// 获取 get / getIn
// 追加 push / unshift / pop / shift / concat
// 修改 set / update
// immutable 中的 List 长度 => istate.size
// 删除 istate.remove( 索引 )
// immutable 结构数据共享 // Map 把 js 中的 json 对象转为 Map 它只能转换 json 对象的第1层 // List 把 js 中的数组转为 List 它只能转换数组的第1层 // fromJS 递归 自动把数组 或 对象转为 Map 或 List // is 专门用来 比对数据 // eslint-disable-next-line import { Map, List, fromJS, is } from 'immutable' let obj = { id: 1, name: '张三', user: { age: 20 } } // 把对象转为 Map Map 它是 immutable 中的对象,不是原生 js 中的对象 // let istate = Map(obj) let istate = fromJS(obj) // console.log(obj,istate); // 获取数据 get / getIn /* console.log('get', istate.get('name')); console.log('getIn', istate.getIn(['name'])); */ console.log('get', istate.get('user').get('age')) console.log('getIn', istate.getIn(['user', 'age'])); // 对象比对 // let obj1 = { id: 1, name: '张三', user: { age: 20 } } // let obj2 = { id: 1, name: '张三', user: { age: 20 } } // console.log(obj1 === obj2) // false 但是它们值是一样的 let obj = { id: 1, name: '张三', user: { age: 20 } } let istate1 = fromJS(obj) let istate2 = fromJS(obj) console.log(is(istate1, istate2)) // true // 修改 -- 任何的修改都会返回一个新的对象 let obj = { id: 1, name: '张三', user: { age: 20 } } let istate = fromJS(obj) // 修改 set / setIn / update / updateIn // set 设置值,不需要原值,update 可以保留原值的基础来进行修改 // let newState = istate.set('name','李四') // let newState = istate.update('name', v => '李四' + v) // 深层数据修改 // let newState = istate.setIn(['user', 'age'], 30) // let newState = istate.updateIn(['user', 'age'], v => v + 1) // toJS 方法,就是把 immutable 对象转为原生 js 对象 console.log(newState.toJS()); // 删除 let obj = { id: 1, name: '张三', user: { age: 20 } } // 普通对象删除属性 // delete obj['name'] let istate = fromJS(obj) let newState = istate.removeIn(['name']) console.log(newState.toJS()); // 合并对象 let obj1 = { id: 1, name: '张三' } let obj2 = { name: '李四', age: 20 } // for in ... Object.assign let istate1 = fromJS(obj1) let istate2 = fromJS(obj2) // 如果两者有相同字段,则以合并中的方法字段为准 let newState = istate1.merge(istate2) console.log(newState.toJS()); // -------------------------------------------------------- // List 类型 对应数组 let arr = [1, 2, { id: 1, name: '张三' }] // // let istate = List(arr) let istate = fromJS(arr) // 获取 get / getIn // console.log(istate.get(1)); // 2 console.log(istate.getIn([2, 'name'])); // 张三 // 追加 push / unshift / pop / shift / concat let newState = istate.push(3) console.log(newState.toJS()); // 修改 set / update // let newState = istate.set(1, 10) // let newState = istate.update(1, v => v + 10) let newState = istate.updateIn([2,'id'], v => v + 10) console.log(newState.toJS()); // immutable 中的 List 长度 console.log(istate.size); // 删除 let newState = istate.remove(1) console.log(newState.toJS());
7.5、redux 中集成 immutable
- 安装 redux-immutable
redux 中利用 combineReducers 来合并 reducer 并初始化 state,redux 自带的 combineReducers只支持 state 是原生 js 形式的,所以需要使用 redux-immutable 提供的 combineReducers 来替换原来的方法。
npm i -S redux-immutable
- 使用
它的合并是支持 immutable 对象合并
import { combineReducers } from 'redux-immutable'
把所有的 reducer 数据转换为 immutable 对象
import {fromJS} from 'immutable'
const defaultState = fromJS({})
图例 :
跳转链接 => React_01 学习笔记
跳转链接 => React_02 学习笔记
跳转链接 => React_04 菜谱项目
跳转链接 => React_05 Hooks