1.组件化
react组件化跟vue有所区别,容器组件 VS 展示组件
状态的维护,数据获取,逻辑的处理尽量在容器组件里面
展示组件尽量是傻瓜式的,纯粹的页面展示的组件,输入props就能知道展示出来是啥样,不包含状态逻辑的处理
2. PureComponent
展示型组件可以配合PureComponent,不过要注意几点:
2-1. 改变引用的数据,PureComponent是没有效果的,比如直接替换整个list数组(可以使用 <Test {...data} /> 用...进行赋值,data是list的单个对象)
2-2. 数据的嵌套层特别多,这样用PureComment反而开销大,尽量要嵌套少,浅层次的
函数式组件,如果要用PureComponent,可以用React.memo进行嵌套
3.高阶组件(HOC)
高阶组件是一个函数,接受一个组件,返回一个新的组件,新的组件可以对属性进行封装,还能重写部分生命周期
高阶组件是可以嵌套的,可以配合装饰器使用 (Babel7)
// MyHigh.js
// 简单的示例,属性name要单独获取(比如从接口Api获取,这里做个简单示例name),进行扩展
// 返回一个组件,是个函数,参数是props
import React from 'react'
const MyHigh=(Com)=>{
const name='自定义名字';
return (props)=> <Com {...props} name={name} />
}
export default MyHigh
// App.js
import Foot from './components/Foot'
import MyHighCom from './components/MyHigh'
const NewFoot=MyHighCom(Foot);
render() {
return (
<div className="wrap">
<NewFoot age={18} />
</div>
)
}
// Foot.js
const Footer = (props) => (
<div>
<span>Show: </span>
<p>{props.name}----{props.age}</p>
</div>
)
4.组件复合,扩展组件 props.children 就是一个合法的js表达式(JSX都是包含在内的)
// 类似 vue的 默认插槽, 用 props.children 展示
import React from 'react'
function Dialog(props){
return <div style={{border:'3px solid green'}}>{props.children}</div>
}
const WelcomeDialog=(props)=>{
return (
<Dialog>
<h1>欢迎光临</h1>
</Dialog>
)
}
export default WelcomeDialog
// 类似vue的具名插槽,就是利用属性传递,可以用在任何位置,下面只是footer举例
// Composition.js
import React from 'react'
function Dialog(props){
return (
<div style={{border:'3px solid green'}}>
{props.children}
{/*类似vue的具名插槽,但是这个属性名字footer要约定,说到底就是传递属性来实现*/}
<div className="footer">{props.footer}</div>
</div>
)
}
const WelcomeDialog=(props)=>{
return (
<Dialog {...props}>
<h1>欢迎光临</h1>
</Dialog>
)
}
export default WelcomeDialog
// 使用
<Composition footer={<button>确定</button>} />
// children是个函数
// 给Fetcher.children 的传入一个对象,在插槽里面使用,类似于vue的作用域插槽
import React from 'react'
const WelcomeDialog=(props)=>{
return (
<Fetcher name='getUser'>
{
({name,age})=>(
<div>{name}---{age}</div>
)
}
</Fetcher>
)
}
const Api={
getUser(){
return {name:'张三',age:18}
}
}
function Fetcher(props){
const user= Api[props.name]();
return props.children(user);
}
export default WelcomeDialog
// 父级组件传递属性,进行统一管理,利用props.children
import React from 'react'
function Radio({children,...rest}){
return (
<label htmlFor={rest.vaule}>
<input id={rest.vaule} type="radio" {...rest} />
{children}
</label>
)
}
function RadioGroup(props){
return (
<div>
{
React.Children.map(props.children,(child)=>{
// child是一个虚拟dom,不可直接添加修改属性,要克隆
return React.cloneElement(child,{name:props.name});
})
}
</div>
)
}
export default ()=>{
return (
<RadioGroup name="mvvm">
<Radio value='vue'>vue</Radio>
<Radio value='react'>react</Radio>
<Radio value='angular'>angular</Radio>
</RadioGroup>
)
}
5. 实例实战 (扩展Form表单,增加验证消息展示)
import React, { Component } from "react";
// 包含在空间外层的,form的直接元素,控制布局和样式之类的,错误消息提示
// children直接展示嵌套的组件,用于扩展
function FormItem(props) {
const { children, status, help } = props;
return (
<div>
{children}
{status === "error" && (
<p style={{ color: "red", fontSize: "12px" }}>{help}</p>
)}
</div>
);
}
// 高阶组件 扩展传入组件功能,返回新的组件
// 增加字段的验证,所有字段的全局验证,表单组件的装饰器[值和name由父级控制]
function MyFormCreate(Com) {
return class extends Component {
constructor(props) {
super(props);
this.state = {};
this.options = {};
}
// 输入框的change事件
handleChange = e => {
const { value, name } = e.target;
this.setState(
{
[name]: value
},
() => {
this.validateField(name); //用回调方式,确保值变了,进行验证
}
);
};
// 全局验证,扩展出去,给使用组件调用
validateAllField = cb => {
const result = Object.keys(this.options).map(name => {
return this.validateField(name);
});
cb(result.every(bool => !!bool), this.state);
};
// 单个字段验证
validateField = name => {
const rules = this.options[name].rules;
const isValid = !rules.some(rule => {
if (rule.required) {
if (!this.state[name]) {
this.setState({
[name + "Message"]: rule.message
});
return true;
}
}
return false;
});
if (isValid) {
this.setState({
[name + "Message"]: ""
});
}
return isValid;
};
// 获取错误信息 给FormItem使用
getErrorMsg = name => {
return this.state[name + "Message"];
};
// 包装表单控件,value和name由父级控制
getFieldDecorator = (name, option, Com) => {
this.options[name] = option;
return (
<div>
{React.cloneElement(Com, {
name: name,
value: this.state[name] || "",
onChange: this.handleChange
})}
</div>
);
};
render() {
return (
<Com
getFieldDecorator={this.getFieldDecorator}
validateAllField={this.validateAllField}
getErrorMsg={this.getErrorMsg}
/>
);
}
};
}
// 装饰器组件 高阶组件的包装
// 使用组件
@MyFormCreate
class MyForm extends Component {
// 提交
submitForm = () => {
this.props.validateAllField((isValid, data) => {
if (isValid) {
console.log("登录:", data);
} else {
console.log("not valid");
}
});
};
render() {
const { getFieldDecorator, getErrorMsg } = this.props;
return (
<div>
<FormItem
status={getErrorMsg("userName") ? "error" : ""}
help={getErrorMsg("userName")}
>
{getFieldDecorator(
"userName",
{
rules: [{ required: true, message: "用户名不能为空" }]
},
<input type="text" />
)}
</FormItem>
<FormItem
status={getErrorMsg("password") ? "error" : ""}
help={getErrorMsg("password")}
>
{getFieldDecorator(
"password",
{
rules: [{ required: true, message: "密码不能为空" }]
},
<input type="password" />
)}
</FormItem>
<button onClick={this.submitForm}>登录</button>
</div>
);
}
}
export default MyForm;
6. redux-thunk
redux-thunk 核心思想是判断是否是函数,如果是个函数action,就先执行,执行完成后就dispatch,如果不是函数是对象,就直接dispatch
// store/index.js
import { createStore, applyMiddleware, combineReducers } from "redux";
import logger from "redux-logger"; //logger组件
import thunk from "redux-thunk"; // 异步组件
import { countReducer } from "./counter"; // counter的reducer
import { userReducer } from "./user"; // user的reducer
const store = createStore(
combineReducers({ count: countReducer, user: userReducer }), // reducer进行组合
applyMiddleware(logger, thunk) // 引用中间件扩展,支持异步,先后顺序是执行顺序
);
export default store;
// redux需要用Provider来指定上下文,确定要传下去的store
// 下面具体内容,在下面React Router会解释
import React, { Component, lazy, Suspense } from "react";
import { Route, BrowserRouter, Switch, Redirect } from "react-router-dom";
import store from "./store";
import { Provider, connect } from "react-redux";
/**
* 生成路由组件
* @param {*} route
*/
export const SubRoute = route => {
return route.private ? (
<PrivateRoute path={route.path} component={route.component} />
) : (
<Route
path={route.path}
render={props => <route.component {...props} routes={route.routes} />}
/>
);
};
export const PrivateRoute = connect(state => ({ isLogin: state.user.isLogin }))(
({ component: Com, isLogin, ...rest }) => {
return (
<Route
{...rest}
render={props =>
isLogin ? (
<Com {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { redirect: props.location.pathname }
}}
/>
)
}
/>
);
}
);
export const lazyComponent = url => lazy(() => import(`${url}`));
const routes = [
{
path: "/",
exact: true,
component: lazyComponent("./Home")
},
{
path: "/redux",
component: lazyComponent("./ReduxTest")
},
{
path: "/login",
component: lazyComponent("./Login")
},
{
path: "/about",
component: lazyComponent("./About"),
private: true
},
{
path: "/detail/:types",
component: lazyComponent("./Detail")
}
];
class App extends Component {
render() {
return (
// 下面进行store的传入
<Provider store={store}>
<BrowserRouter>
<Suspense fallback={<div>loading...</div>}>
<Switch>
{routes.map((route, i) => (
<SubRoute key={i} {...route} />
))}
</Switch>
</Suspense>
</BrowserRouter>
</Provider>
);
}
}
export default App;
// store/counter.js
import { COUNT_ADD, COUNT_REDUCE } from "./types";
export const countReducer = (state = 0, action) => {
switch (action.type) {
case COUNT_ADD:
return state + 1;
case COUNT_REDUCE:
return state - 1;
default:
return state;
}
};
// store/user.js
import { REQUEST_LOGIN, LOGIN } from "./types";
export const userReducer = (state = { isLogin: false, loading: false }, action) => {
switch (action.type) {
case REQUEST_LOGIN:
return {
isLogin: false,
loading: true
};
case LOGIN:
return {
isLogin: true,
loading: false
};
default:
return state;
}
};
// action
export const login=()=>(dispatch)=>{
dispatch({type: REQUEST_LOGIN});
// 异步登录操作模拟
setTimeout(() => {
dispatch({type: LOGIN});
}, 2000);
}
// store/types.js
// counter.js
export const COUNT_ADD='COUNT_ADD';
export const COUNT_REDUCE='COUNT_REDUCE';
// login.js
export const REQUEST_LOGIN='REQUEST_LOGIN';
export const LOGIN='LOGIN';
// ReduxTest.js 使用redux的组件
import React, { Component } from "react";
import { connect } from "react-redux"; // react和redux进行连接
import { COUNT_REDUCE, COUNT_ADD } from "./store/types";
const mapStateToProps = state => ({ num: state.count });
// state转化成props
const mapDispatchToProps = {
add: () => ({ type: COUNT_ADD }),
reduce: () => ({ type: COUNT_REDUCE }),
asyncAdd: () => dispatch => {
setTimeout(() => {
dispatch({ type: COUNT_ADD });
}, 1500);
}
};
// action转化成props
// 这边用的是babel的高阶组件的装饰器
@connect(
mapStateToProps,
mapDispatchToProps
)
class ReduxTest extends Component {
render() {
const { num, add, reduce, asyncAdd } = this.props;
return (
<div>
<p>{num}</p>
<button onClick={add}>+</button>
<button onClick={reduce}>-</button>
<button onClick={asyncAdd}>async +</button>
</div>
);
}
}
export default ReduxTest;
7. React Router
import React, { Component, lazy, Suspense } from "react";
import { Route, BrowserRouter, Switch, Redirect } from "react-router-dom";
import store from "./store";
import { Provider, connect } from "react-redux";
/**
* 生成路由组件
* @param {*} route
*/
export const SubRoute = route => {
//如果是权限组件就用PrivateRoute的高阶组件,不是的话就直接是Route
return route.private ? (
<PrivateRoute path={route.path} component={route.component} />
) : (
<Route
path={route.path}
render={props => <route.component {...props} routes={route.routes} />}
/>
);
};
/**
* 高阶组件,判断是否登录,登录了就直接跳转到路由指定的页面,没有登录就跳转登录页面
* 跳转登录页面时候,把要登录后跳转的页面路径传过去
* connect是react-redux的api,传入redux的用户信息isLogin
*/
export const PrivateRoute = connect(state => ({ isLogin: state.user.isLogin }))(
({ component: Com/*组件需要大写,做个命名的映射*/, isLogin, ...rest }) => {
return (
<Route
{...rest}
render={props =>
isLogin ? (
<Com {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { redirect: props.location.pathname }
}}
/>
)
}
/>
);
}
);
/**
* 懒加载Api
* import里面必须是字符串,要不然要报错
* @param {*} url
*/
export const lazyComponent = url => lazy(() => import(`${url}`));
// 页面的数组数据
const routes = [
{
path: "/",
exact: true,
component: lazyComponent("./Home")
},
{
path: "/redux",
component: lazyComponent("./ReduxTest")
},
{
path: "/login",
component: lazyComponent("./Login")
},
{
path: "/about",
component: lazyComponent("./About"),
private: true
},
{
path: "/detail/:types",
component: lazyComponent("./Detail")
}
];
/**
* 路由没有匹配到,暂时展示这个
* @param {*} param0
*/
function NoMatch({ location }) {
return (
<h3>没有匹配到当前的路由:{location.pathname}</h3>
)
}
// App组件,根级别
class App extends Component {
render() {
return (
<Provider store={store}>
<BrowserRouter>
<Suspense fallback={<div>loading...</div>}>
{/*Switch 包含里面只能匹配一个*/}
<Switch>
{routes.map((route, i) => (
<SubRoute key={i} {...route} />
))}
{/*不指定路由,就会完美匹配*/}
<Route component={NoMatch}></Route>
</Switch>
</Suspense>
</BrowserRouter>
</Provider>
);
}
}
export default App;
// Home.js
import React, { Component } from "react";
import { Link } from "react-router-dom";
export class Home extends Component {
render() {
return (
<ul>
<li>
<Link to="/">Home</Link>
</li>
{/*to的Object形式,传数据到state,之后从location.state里面拿*/}
<li>
<Link
to={{ pathname: "/redux", state: { msg: "这是个redux的组件" } }}
>
Redux
</Link>
</li>
{/*about是个复合嵌套页面,里面自带路由*/}
<li>
<Link to="/about">About</Link>
</li>
{/*带参数的路由detail*/}
<li>
<Link to="/detail/history">Detail/History</Link>
</li>
<li>
<Link
to={{
pathname: "/detail/location",
state: { title: "当前是locaion" }
}}
>
Detail/Location
</Link>
</li>
<li>
<Link to="/detail/match">Detail/Match</Link>
</li>
</ul>
);
}
}
export default Home;
// About.js
// 这是个嵌套复合页面
// 这是个权限路由,没有登录,会先去登录页面,登录好了在跳转过来,如果已经登录过了,就直接跳过来
// 下面的图片的console里面,就是登录的过程(redux-logger)
import React, { Component } from "react";
import { Link, Switch, Route, Redirect } from "react-router-dom";
class About extends Component {
render() {
return (
<div>
<h3>个人中心</h3>
<p><Link to="/about/me">个人中心</Link></p>
<p>
<Link to="/about/order">订单系统</Link>
</p>
{/*嵌套路由*/}
<Switch>
<Route path="/about/me" component={() => <div>Me</div>}></Route>
<Route path="/about/order" component={() => <div>订单</div>}></Route>
<Redirect to="/about/me" />{/*上面都不满足,就跳转/about/me,比如/about*/}
</Switch>
</div>
);
}
}
export default About;
// Detail.js
// 这是参数不同的页面,动态
import React, { Component } from "react";
export class Detail extends Component {
render() {
const { history, location, match } = this.props;
return <div>
{match.params.types==='history' && <button onClick={history.goBack}>Histroy进行回退</button>}
{match.params.types==='location' && <p>{JSON.stringify(location)}</p>}
{match.params.types==='match' && <p>{JSON.stringify(match)}</p>}
</div>;
}
}
export default Detail;
// ReduxTest.js
// 前面已经介绍过
import React, { Component } from "react";
import { connect } from "react-redux";
import { COUNT_REDUCE, COUNT_ADD } from "./store/types";
const mapStateToProps = state => ({ num: state.count });
const mapDispatchToProps = {
add: () => ({ type: COUNT_ADD }),
reduce: () => ({ type: COUNT_REDUCE }),
asyncAdd: () => dispatch => {
setTimeout(() => {
dispatch({ type: COUNT_ADD });
}, 1500);
}
};
@connect(
mapStateToProps,
mapDispatchToProps
)
class ReduxTest extends Component {
render() {
const { num, add, reduce, asyncAdd } = this.props;
return (
<div>
<p>{num}</p>
<button onClick={add}>+</button>
<button onClick={reduce}>-</button>
<button onClick={asyncAdd}>async +</button>
</div>
);
}
}
export default ReduxTest;
// Login.js 上面的MyForm直接引用,加点逻辑
import React, { Component } from "react";
import { connect } from "react-redux";
import { login } from "./store/user";
import { Redirect } from "react-router-dom";
// 包含在空间外层的,form的直接元素,控制布局和样式之类的,错误消息提示
function FormItem(props) {
const { children, status, help } = props;
return (
<div>
{children}
{status === "error" && (
<p style={{ color: "red", fontSize: "12px" }}>{help}</p>
)}
</div>
);
}
// 高阶组件 扩展传入组件功能,返回新的组件
// 增加字段的验证,所有字段的全局验证,表单组件的装饰器[值和name由父级控制]
function MyFormCreate(Com) {
return class extends Component {
constructor(props) {
super(props);
this.state = {};
this.options = {};
}
// 输入框的change事件
handleChange = e => {
const { value, name } = e.target;
this.setState(
{
[name]: value
},
() => {
this.validateField(name); //用回调方式,确保值变了,进行验证
}
);
};
// 全局验证,扩展出去,给使用组件调用
validateAllField = cb => {
const result = Object.keys(this.options).map(name => {
return this.validateField(name);
});
cb(result.every(bool => !!bool), this.state);
};
// 单个字段验证
validateField = name => {
const rules = this.options[name].rules;
const isValid = !rules.some(rule => {
if (rule.required) {
if (!this.state[name]) {
this.setState({
[name + "Message"]: rule.message
});
return true;
}
}
return false;
});
if (isValid) {
this.setState({
[name + "Message"]: ""
});
}
return isValid;
};
// 获取错误信息 给FormItem使用
getErrorMsg = name => {
return this.state[name + "Message"];
};
// 包装表单控件,value和name由父级控制
getFieldDecorator = (name, option, Com) => {
this.options[name] = option;
return (
<div>
<p>{this.state[name]}---state</p>
{React.cloneElement(Com, {
name: name,
value: this.state[name] || "",
onChange: this.handleChange
})}
</div>
);
};
render() {
return (
<Com
{...this.props}
getFieldDecorator={this.getFieldDecorator}
validateAllField={this.validateAllField}
getErrorMsg={this.getErrorMsg}
/>
);
}
};
}
// 装饰器组件 高阶组件的包装
// 使用组件
@connect(
state => ({ isLogin: state.user.isLogin, loading: state.user.loading }),
{ login }
)
@MyFormCreate
class MyForm extends Component {
// 提交
submitForm = () => {
this.props.validateAllField((isValid, data) => {
if (isValid) {
console.log("登录:", data);
console.log(this.props)
this.props.login();
} else {
console.log("not valid");
}
});
};
render() {
const { getFieldDecorator, getErrorMsg, isLogin, location, loading } = this.props;
if (isLogin) {
return <Redirect to={location.state.redirect || "/"} />;
}
return (
<div>
<FormItem
status={getErrorMsg("userName") ? "error" : ""}
help={getErrorMsg("userName")}
>
{getFieldDecorator(
"userName",
{
rules: [{ required: true, message: "用户名不能为空" }]
},
<input type="text" />
)}
</FormItem>
<FormItem
status={getErrorMsg("password") ? "error" : ""}
help={getErrorMsg("password")}
>
{getFieldDecorator(
"password",
{
rules: [{ required: true, message: "密码不能为空" }]
},
<input type="password" />
)}
</FormItem>
<button onClick={this.submitForm} disabled={loading}>{loading?'登录中...':'登录'}</button>
</div>
);
}
}
export default MyForm;