react
安装
$ cnpm install -g create-react-app
$ create-react-app my-demo
$ cd my-demo/
$ npm start
react官方文档很好理解,并且在下边的学习中也会应用到react的知识,在此不再过多的赘述。
redux
redux 用普通对象来描述state。
想要更改state中的数据,需要发起一个 action 。action也是一个普通对象,用于描述发生了什么。强制使用action来描述所有变化可以让我们清晰知道到底发生了什么变化。
为了把 action 和 state 串起来,开发 reducer 函数,接收 state 和 action ,并返回新的 state。对于大的应用,我们不可能只写一个reducer 函数,可以些很多个小函数分别管理state的一部分,然后用一个reducer调用,从而管理整个 state。
function todos1(state, action) {}
function todos2(state, action) {}
function reducerAll(state, action) {
return {
todos1: todos1(state.todos1, action),
todos2: todos2(state.todos2, action),
};
}
三大原则
单一数据源
整个应用的 state 存储在一棵 object tree 中,并且这个object tree 只存在于唯一一个store 中。
State是只读的
改变state的唯一办法是触发action。任何视图和网络请求都不能直接修改state,只能表达修改的意图。
用纯函数来执行修改
reducers 用来描述action 如何改变 state tree。
reducer 是纯函数,接收旧的state 和 action 返回新的state。
开始可以只写一个reducer,随着应用的变大,可以把他拆成多个小的 reducers,分别操作state tree的不同部分。
基本概念
Action
- 通过
store.dispatch将action(含数据)送到store。是store数据的唯一来源。 - 本质上是
JavaScript普通对象,必须使用一个字符串类型的type字段表示要执行的动作(并没有更新state)。一般将type定义成字符串常量。项目规模大时,使用单独的模块或者文件来存放action。(使用单独的模块或文件来定义action type常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做action type更方便些。不过,在大型应用中把它们显式地定义成常量还是利大于弊的。) - 尽量少在
Action中传递数据。 Action创建函数:返回action的函数。
Reducer
-
接收旧的
state和action,返回新的state。 -
保持
reducer纯净非常重要。永远不要在reducer里做这些操作(只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。):- 修改传入参数;
- 执行有副作用的操作,如
API请求和路由跳转; - 调用非纯函数,如
Date.now()或Math.random()。
-
注意:
-
不修改
state:Object.assign({},state,{data}),因为第一个参数的值会改变,必须把第一个参数设置为空对象。- 用
{...state,...newState}来更新数据。
-
**在
default情况下返回旧的state:**遇到未知的action时一定要返回旧的state。
-
-
只写一个
reducer会让代码看起来冗长,可以进行拆分。每个reducer只负责全局state中他负责的一部分,每个reducer对应的state参数都不同,分别对应他管理的那部分state数据。 -
用
combineReducers()工具类合并reducer。-
combineReducers接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过export暴露出每个 reducer 函数,然后使用import * as reducers得到一个以它们名字作为key的object:import { combineReducers } from 'redux' import * as reducers from './reducers' const todoApp = combineReducers(reducers)
-
Store
action 来描述发生什么,reducers根据action 更新state。Store是联系action和reducers的对象。有以下职责:
- 维持应用的
state - 提供
getState()方法获取state - 提供
dispatch(action)方法更新state - 通过
subscribe(listener)注册监听器 - 通过
subscribe(listener)返回的函数注销监听器
Redux应用只有单一的store。当需要拆分数时,应该用reducer组合,而不是创建多个store
通过reducer创建store非常容易:
import { createStore } from 'redux'
import todoApp from './reducers' // combineReducers 将多个 reducer 合并成一个
let store = createStore(todoApp)
createStore的第二个参数是可选的, 用于设置 state 初始状态。也可以用 reducer传入的state初始化相应的state,不初始化默认为 undefined。
数据流
严格的单向数据流是 Redux 架构的设计核心。
Redux 应用中数据的生命周期遵循下面 4 个步骤:
- 调用
store.dispatch(action):可以在认个地方调用 Redux store调用传入的reducer函数。- 根
reducer应该把多个子reducer输出合并成一个单一的state树。 Redux store保存了根reducer返回的完整state树。
代码理解
借助代码理解redux。用create-react-app创建一个react应用。
场景
store中存数组数据cart,我们利用redux 进行管理,实现增加、删除和更新的操作。
Actions
// src/actions/cartActions.js
// 定义action.type 字符常量
export const ADD_TO_CART = "ADD_TO_CART";
export const DELETE_FROM_CART = "DELETE_FROM_CART";
export const UPDATE_CART = "UPDATE_CART";
// action 创建函数
export function addToCart(product, quantity, unitCost) {
return {
type: ADD_TO_CART,
payload: { product, quantity, unitCost },
};
}
export function deleteFromCart(product) {
return {
type: DELETE_FROM_CART,
payload: {
product,
},
};
}
export function updateCart(product, quantity, unitCost) {
return {
type: UPDATE_CART,
payload: {
product,
quantity,
unitCost,
},
};
}
Reducer
// src/reducers/cartReducers.js
import {
ADD_TO_CART,
UPDATE_CART,
DELETE_FROM_CART,
} from "../actions/cartActions";
// state 初始化数据
const initialState = {
cart: [
{
product: "bread 700g",
quantity: 2,
unitCost: 90,
},
{
product: "milk 500ml",
quantity: 1,
unitCost: 47,
},
],
};
// 根据action更新state,reducer传入的第一个参数可以用于初始化state,否则对应state为undefined
export default function (state = initialState, action) {
switch (action.type) {
case ADD_TO_CART: {
return {
...state,
cart: [...state.cart, action.payload],
};
}
case UPDATE_CART: {
return {
...state,
cart: state.cart.map((item) =>
item.product === action.payload.product ? action.payload : item
),
};
}
case DELETE_FROM_CART: {
return {
...state,
cart: state.cart.filter(
(item) => item.product !== action.payload.product
),
};
}
default:
return state;
}
}
假设存在别的reducer:
// src/reducers/productsReducer.js
export default function(state = [], action) {
return state;
};
需要用combineReducers()整合:
// src/reducers/index.js
import {combineReducers} from 'redux'
import productsReducer from './productsReducer';
import cartReducer from './cartReducers'
const allReducers = {
products: productsReducer,
shoppingCart: cartReducer,
};
const rootReducer = combineReducers(allReducers);
export default rootReducer
store
// src/store.js
import {createStore} from 'redux'
import rootReducer from './reducers/index'
let store = createStore(rootReducer);
export default store
dispatch
import store from "./store";
import { addToCart,updateCart,deleteFromCart } from "./actions/cartActions";
console.log("initialState", store.getState());
// 订阅
let unsubscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(addToCart("coffee 500gm", 1, 250));
store.dispatch(addToCart("flour 100g", 1, 250));
store.dispatch(addToCart("juice 2L", 1, 250));
store.dispatch(updateCart("flour 100g", 100, 250));
store.dispatch(deleteFromCart("coffee 500gm", 1, 250))
//解除订阅
unsubscribe();
代码完成后,执行npm start运行,可以看到打印的日志。
React-Redux
-
安装
react-redux:npm install --save react-redux
-
写
react代码,并用Provider类将react应用程序包装在redux容器中:import React from 'react'; import ReactDom from 'react-dom'; import {Provider} from 'react-redux'; const App = <h1>Redux Shopping Cart</h1> ReactDom.render( <Provider store={store}> {App} </Provider>, document.getElementById('root') )
以上完成了简单的集成。接下来详细的学习react-redux。
安装
react-redux并不是redux内置包,需要单独安装:
npm install --save react-redux
API
<provider store>组件。
根组件嵌套在<Provider> 中才可以使用 connect()方法。唯一的作用是传递store。
-
普通
React:ReactDOM.render( <Provider store={store}> <MyRootComponent /> </Provider>, rootEl ) -
React RouterReactDOM.render( <Provider store={store}> <Router history={history}> <Route path="/" component={App}> <Route path="foo" component={Foo}/> <Route path="bar" component={Bar}/> </Route> </Router> </Provider>, document.getElementById('root') )
参数:
store:应用程序全局唯一的 Redux-Store
children:组件层级的根组件
mapStateToProps(state, [ownProps])
store到组件props的映射:监听Redux store的变化,Redux store发生改变,mapStateToProps函数会被调用,- 返回一个纯对象,这个对象会与组件的
props合并。 - 如果指定了该回调函数中的第二个参数
ownProps,则该参数的值为传递到组件的props,而且只要组件接收到新的props,mapStateToProps也会被调用
mapDispatchToProps()
- 分发
action,即组件到store的映射。
connect()
React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来
实例:计数器
实现一个计数器,计数值由store映射,点击increase按钮通过dispatch将state递增。
UI组件
- 只负责 UI 的呈现,不带有任何业务逻辑
- 没有状态(即不使用
this.state这个变量) - 所有数据都由参数(
this.props)提供 - 不使用任何
Redux的API
const { Component } = require("react");
export default class Counter extends Component{
render(){
const {value,onIncreaseClick} = this.props
return(
<div>
<span>{value}</span>
<button onClick = {onIncreaseClick}>Increase</button>
</div>
)
}
}
此组件中显示的 value由state计算得到,onIncreaseClick 向外发出Action。
容器组件
- 负责管理数据和业务逻辑,不负责 UI 的呈现
- 带有内部状态
- 使用
Redux的API connect方法生成容器组件
import { connect } from 'react-redux';
import {increaseAction} from '../actions'
import Counter from '../components/count';
function mapStateToProps(state) {
return {
value: state.count,
};
}
function mapDispatchToProps(dispatch){
return {
onIncreaseClick:()=>dispatch(increaseAction)
}
}
// App 是一个组件,Counter的容器组件
const App =connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
export default App
Action
export const increaseAction = {
type:'increase'
}
Reducer
export default function counter(state = { count: 0 }, action) {
const count = state.count;
switch (action.type) {
case "increase":
return { count: count + 1 };
default:
return state;
}
}
Store
import {createStore} from 'redux'
import counter from './reat-rdx/reducers/index'
const store = createStore(counter);
export default store
组件中使用并用Provider包含
import App from './reat-rdx/containerComponents/index'
ReactDom.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById("root")
);
总结:
在我理解,react-redux是在 UI 组件外层包含一层容器组件来对state进行处理,并进行action分发。
React Router
React Router 4.x 和之前是两个完全不同的体系,本文章主要学习4.x,想学2.x的可以看这篇介绍
基本路由
三个基本页面:主页,关于,用户。如下
// Home.js
import { Component } from "react";
export default class Home extends Component {
render() {
return <h2>Home</h2>;
}
}
// User.js
import { Component } from "react";
export default class User extends Component {
render() {
return <h2>User</h2>;
}
}
// About.js
import { Component } from "react";
export default class About extends Component {
render() {
return <h2>About</h2>;
}
}
控制渲染:
// App.js
import Home from "./Home";
import Users from "./Users";
import About from "./About";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link> //点击这个Link时,路由变为:/about
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
</nav>
{/* switch 通过Route 渲染匹配当前URL的组件 */}
<Switch>
<Route path="/about"> // 路由为/about时,加载About组件
<About />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</Router>
);
}
// index.js
import React from "react";
import ReactDom from "react-dom";
import App from './reat-rout/modules/App'
ReactDom.render(
<App/>,
document.getElementById('root')
)
总结:
-
<Link to="/about">About</Link> //点击这个Link时,路由变为:/about <Route path="/about"> // 路由为/about时,加载About组件
路由嵌套
添加 Topics 组件:
import {
Route,
Link,
Switch,
useParams,
useRouteMatch,
} from "react-router-dom";
export default function Topics() {
let match = useRouteMatch(); // 只在函数中可以使用
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${(match.url)}/props-v-state`}>Props v.State</Link>
</li>
</ul>
{/* Topics有自己的switch */}
<Switch>
<Route path={`${match.path}/:topicId`}>
<Topic />
</Route>
<Route path={match.path}>
<h3>please select a topic</h3>
</Route>
</Switch>
</div>
);
}
function Topic() {
let { topicId } = useParams();
return <h3>requested topic ID :{topicId}</h3>;
}
显示:
import Home from "./Home";
import Users from "./Users";
import About from "./About";
import Topics from "./Topics";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
</nav>
{/* switch 通过Route 渲染匹配当前URL的组件 */}
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/topics">
<Topics />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</Router>
);
}
总结:
我们在这一部分看到了以下生面孔,不要着急先继续往下看,后便会有详细解答
-
let match = useRouteMatch(); -
<Link to={`${match.url}/components`}>Components:{match.url}</Link> -
<Route path={`${match.path}/:topicId`}> -
let { topicId } = useParams();
主要成分
React Router中的组件主要分为三类:
- 路由器,像
<BrowserRouter>和<HashRouter> - 路线匹配器,例如
<Route>和<Switch> - 导航,例如
<Link>,<NavLink>和<Redirect>
路由器
-
<BrowserRouter>使用常规的URL路径。这些通常是外观最好的URL,但是它们要求正确配置服务器。 -
<HashRouter>将当前位置存储在URL的hash一部分中,因此URL看起来像http://example.com/#/your/page。由于哈希从不发送到服务器,因此这意味着不需要特殊的服务器配置。 -
要使用路由器,只需确保将其呈现在元素层次结构的根目录下。
路线匹配器
-
有两个路由匹配组件:
Switch和Route。当<Switch>被渲染,它会搜索其children<Route>内容找到一个其path当前的URL匹配。当找到一个对象时,它将渲染该对象<Route>而忽略所有其他对象。 -
所以要将
<Route>的特定性更高的放在比较靠前位置。 -
如果没有
<Route>匹配项,则<Switch>不会呈现任何内容。
<Switch>
// 如果当前URL是/about,则呈现此路由其他的都被忽略了
<Route path="/about">
<About />
</Route>
// 注意这两条路线是如何排序的。更具体的path=“/contact/:id”位于path=“/contact”之前,因此当查 看单个联系人时,将呈现路由/contact/:id
<Route path="/contact/:id">
<Contact />
</Route>
<Route path="/contact">
<AllContacts />
</Route>
// 如果之前的路径都没有渲染任何内容,
这条路线起到了后备的作用。重要提示:路径为“/”的路线将始终匹配URL,因为所有URL都以/开头。这就 是为什么我们把这个放在最后
<Route path="/">
<Home />
</Route>
</Switch>
需要注意的重要一件事是a<Route path>匹配URL的开头,而不是整个开头。因此,<Route path="/">将始终与网址匹配。因此,我们通常将这放在<Route>最后<Switch>。
导航(路线更改器)
-
所谓的导航就是
Link组件,类似于<a>标签。<Link to="/">Home</Link> // <a href="/">Home</a> -
<NavLink>是一种特殊类型的<Link>,当其to prop与当前位置匹配时,可以将自己设置为“active”。<NavLink to="/react" activeClassName="hurray"> React </NavLink> // When the URL is /react, this renders: // <a href="/react" className="hurray">React</a> // When it's something else: // <a href="/react">React</a> -
任何时候要强制导航,都可以渲染
<Redirect>。当<Redirect>渲染时,它将使用其to prop进行导航。<Redirect to="/login" />
API
钩子
React Router附带了一些钩子,可让您访问路由器的状态并从组件内部执行导航。
useHistoryuseLocationuseParamsuseRouteMatch
useHistory
访问history,可用于导航。
import { useHistory } from "react-router-dom";
export default function HomeBtn() {
let history = useHistory();
function handleClick() {
history.push("/");
}
return <button onClick={handleClick}>go Home</button>;
}
useLocation
返回代表当前URL的对象,当URL改变的时候返回新的location对象。
import { useLocation } from "react-router-dom";
export default function UseLocation() {
let location = useLocation();
return (
<h3>
location:{location.pathname}
{console.log(location)}
</h3>
);
}
useParams
返回URL参数的键/值对的对象。可用于匹配路由的参数。
<Route path="topics/:topicId">
<Topic />
</Route>
function Topic() {
let { topicId } = useParams();
return <h3>requested topic ID :{topicId}</h3>;
}
这是前文【路由嵌套】部分用到的,当路由为:http://localhost:3000/topics/props-v-state时,topicId为props-v-state
useRouteMatch
useRouteMatch钩子尝试以与<Route>相同的方式匹配当前URL。它对于在不实际渲染<Route>的情况下访问匹配数据非常有用。
除了钩子函数之外还有很多API,想了解的可以去官网查看。