React 组件化开发(二):最新组件api

2019-07-18 17:55:36 浏览数 (1)

学习的过程,就是把已经实现的功能反复地,变着花样地重构,直到找到最合适的点。

如果连这点觉悟都没有,那就不是一个合格的程序员。而雇主的本质是逐利,最忌讳的是重构,这个问题可以请高水平的工程师来得到缓解,但不可能彻底解决。

本文知识要点

  • Hook
  • 高阶组件
  • 组件通信
  • 上下文
  • React.cloneElement

Hook

文档地址:https://zh-hans.reactjs.org/docs/hooks-intro.html#___gatsby

hook是16.8版本新增的特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

它具有如下特点:

  • 在无需修改状态的情况下,复用状态逻辑。
  • 将相关联的部分拆分为更小的函数,复杂组件将更容易理解。
  • 更简洁,更易理解。
状态钩子 State Hook

函数型组件可以使用状态:

代码语言:javascript复制
function Example() {
    // 声明一个新的叫做 “count” 的 state 变量,
    // 数组第二个值是变更函数,参数接收的是新状态
    // useState参数是初始值。
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count   1)}>
                Click me
      </button>
        </div>
    );
}

你点击一次,count就加1.这样就在函数型组件中实现了state。

你可以执行更加复杂的操作:

代码语言:javascript复制
// 添加水果
function AddFruit({setFruits,fruits}){

    return (
        <input type='text' onKeyUp={(e)=>{
            if(e.keyCode==13){
                setFruits(fruits.concat(e.target.value))
                e.target.value='';
            }
            e.persist()
        }}/>
    )
}

export default function HooksTest() {
    const [fruit, setFruit] = useState("草莓");
    const [fruits, setFruits ] = useState(['草莓','苹果','鸭梨']);

    return (
        <div>
            <p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
            <AddFruit  setFruits={setFruits} fruits={fruits}></AddFruit>
            <FruitList setFruit={setFruit} fruits={fruits}></FruitList>
        </div>
    );
}

如果用以前的写法,难以想象,用这么短的代码就实现了一个购物车。

副作用钩子 Effect Hook (类似watch)

函数组件执行副作用操作。

副作用是什么鬼?它包括数据获取,设置订阅,手动更改dom等。最典型的就是异步数据获取

基本使用

代码语言:javascript复制
import { useEffect } from "react";

export default function HooksTest() {
    const [fruit, setFruit] = useState("草莓");
    const [fruits, setFruits] = useState([]);
    // 使用useEffect异步获取数据
    useEffect(() => {
        setTimeout(() => {
            setFruits(['香蕉', '苹果'])
        }, 1000);
    })
    return (
        <div>
            <p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
            <AddFruit setFruits={setFruits} fruits={fruits}></AddFruit>
            <FruitList setFruit={setFruit} fruits={fruits}></FruitList>
        </div>
    );
}

这样就实现了异步获取数据.

这个和直接settimout有什么区别呢?如果在useEffect中,会发现不断在执行(每隔一秒),如果执行点击,他会越来越快。

代码语言:javascript复制
export default function HooksTest() {
    const [fruit, setFruit] = useState("草莓");
    const [fruits, setFruits] = useState(['草莓','香蕉']);
    // 使用useEffect异步获取数据
    useEffect(() => {
        document.title=fruit;
    },[fruit])
    return (
        <div>
            <p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
            <AddFruit setFruits={setFruits} fruits={fruits}></AddFruit>
            <FruitList setFruit={setFruit} fruits={fruits}></FruitList>
        </div>
    );
}

清务必设置依赖选项(在何时执行),如果没有则放一个空数组。

这在设置做分页时非常管用

清除依赖:

代码语言:javascript复制
useEffect(()=>{...}, [])

useEffect(() => {
    const timer = setInterval(() => {
        console.log('msg');
    }, 1000);
    return function(){
        clearInterval(timer);
} }, []);
useReducer (状态管理lowb实现)

useState的可选项,常用于组件有复杂状态逻辑时,类似于redux中reducer概念。

在redux中,reducer类似vuex中的mutation,接收action,改变state。

代码语言:javascript复制
import { useReducer } from "react";
// 状态维护reducer
function fruitReducer(state, action) {
    switch (action.type) {
        case "init":
            return action.payload;
        case "add":
            return [...state, action.payload];
        default:
            return state;
    }
}

// 添加水果
function AddFruit({ onAddFruit, fruits }) {

    return (
        <input type='text' onKeyUp={(e) => {
            if (e.keyCode == 13) {
                onAddFruit(e.target.value)
                e.target.value = '';
            }
            e.persist()
        }} />
    )
}

export default function HooksTest() {
    const [fruits, dispatch] = useReducer(fruitReducer, []);
    const [fruit, setFruit] = useState("草莓");
    useEffect(() => {
        setTimeout(() => {
            // 变更状态,提交
            dispatch({ type: "init", payload: ["香蕉", "西瓜"] });
        }, 1000);
    }, []);
    return (
        <div>
            {/*变更状态*/}
            <AddFruit onAddFruit={pname => dispatch({ type: 'add', payload: pname })}  fruits={fruits}></AddFruit>
            <FruitList setFruit={setFruit} fruits={fruits}></FruitList>
        </div>
    );
}

实现的功能完全一样。但是一个全局的状态就实现了共享。

useContext

上面有个问题,就是AddFruit组件与父组件存在耦合。这时应该考虑解耦的问题。

useContext用于在快速在函数组件中导入上下文。把provide作为所有元素的老爹。隔代传参。

代码语言:javascript复制
import React, { useContext } from "react"; // 创建上下文
const Context = React.createContext();
export default function HooksTest() {
    // ...
    return (
        {/* 提供上下文的值 */ }
        < Context.Provider value = {{ fruits, dispatch }}>
          <div>
            {/* 这里不再需要给FruitAdd传递变更函数,实现了解耦 */}
              <AddFruit />
          </div>
            </Context.Provider >
); }
function AddFruit(props) {
    // 获取上下文
    const { dispatch } = useContext(Context) 
    const onAddFruit = e => {
        if (e.key === "Enter") {
            // 直接派发动作修改状态
            dispatch({ type: "add", payload: pname }) setPname("");
        }
    };
    // ...
}

清爽多了!

不过对于傻瓜组件,可以不考虑接耦。也不见得这种方法完全取代redux。

React表单组件设计

除了重构,还有一个重要的地方是造轮子。

antd的表单实现
  1. import React from 'react'
  2. import antd from 'antd'
  3. const { Form, Icon, Input, Button} = antd;
  4. class NormalLoginForm extends React.Component {
  5. handleSubmit = e => {
  6. e.preventDefault();
  7. this.props.form.validateFields((err, values) => {
  8. if (!err) {
  9. console.log('Received values of form: ', values);
  10. }
  11. });
  12. };
  13. render() {
  14. const { getFieldDecorator } = this.props.form;
  15. return (
  16. <Form onSubmit={this.handleSubmit} className="login-form">
  17. <Form.Item>
  18. {getFieldDecorator('username', {
  19. rules: [{ required: true, message: 'Please input your username!' }],
  20. })(
  21. <Input
  22. prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
  23. placeholder="Username"
  24. />,
  25. )}
  26. </Form.Item>
  27. <Form.Item>
  28. {getFieldDecorator('password', {
  29. rules: [{ required: true, message: 'Please input your Password!' }],
  30. })(
  31. <Input
  32. prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
  33. type="password"
  34. placeholder="Password"
  35. />,
  36. )}
  37. </Form.Item>
  38. <Form.Item>
  39. <Button type="primary" htmlType="submit" className="login-form-button">
  40. Log in
  41. </Button>
  42. </Form.Item>
  43. </Form>
  44. );
  45. }
  46. }
  47. const WrappedNormalLoginForm = Form.create({ name: 'normal_login' })(NormalLoginForm);
  48. export default WrappedNormalLoginForm;

这是一个带有完整校验功能的表单。开发表单组件,至少考虑三个问题:

  • 数据收集
  • 校验
  • 提交

表单的结构如下

代码语言:javascript复制
| - Form
  |-FormItem
   |-校验规则渲染下的表单组件

校验是怎么实现的?留意 getFieldDecorator:作用是封装表单组件为更强功能(可校验)的组件。

代码语言:javascript复制
const { getFieldDecorator } = this.props.form;

代码里没有提及 this.props.form是如何创建的,这其实是一个高阶组件,参数包括字段名/校验规则/返回的表单元素。

倒数第二行:

代码语言:javascript复制
const WrappedNormalLoginForm = Form.create({ name: 'normal_login' })(NormalLoginForm);

通过一个create工厂函数变成一个高阶组件,返回功能正式的组件。

设计思想:假设有一个组件,只管样式。通过高阶组件的处理,就成了一个完整功能的表单。

如何收集数据?那得看提交方法:

代码语言:javascript复制
handleSubmit = e => {
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Received values of form: ', values);
      }
    });
  };

又是 this.props.form提供了一个 validateFields方法。包括校验结果 errvalues值。

造轮子第一步

做一个类似antd的表单组件,不妨叫他为 dantd.

需求:先实现一个登录表单吧!

代码语言:javascript复制
import React, { Component } from "react";



export default class DFormTest extends Component {
    render() {
        return (
            <div>
                <input type="text" /> 
                <input type="password" /> 
                <button>登录</Button>
            </div>
        );
    }
}

那么就有一个样子了。

getFieldDec怎样才能加载上到form上?

于是问题就是做高阶组件,可以扩展现有表单,包括以上三个功能:

  • 控件包装
  • 时间处理
  • 事件处理

于是我们可以着手来写这个高阶组件函数

代码语言:javascript复制
function dFormCreate(Component){
    return class extends React.Component{
        constructor(props){
            super(props)
            // 期望用户能给传配置选项
            this.option={};
            this.state={};
        }
        /**
         * 包装函数
         * 接收字段名/校验配置
         * 返回一个高阶组件
         */
        getFieldDec=(field,option)=>{
            this.option[field]=option;//选项告诉我们如何校验
            return InputComp =>(
                <div>
                    {/* vdom不能修改,克隆一份再扩展 */}
                    {React.cloneElement(InputComp,{
                        name:field,
                        value:this.state[field]||'',
                        onChange:this.handleChange //执行校验,设置状态
                    })}
                </div>
            )
        }

        render(){

            return (
                <Component {...this.props} getFieldDec={this.getFieldDec}/>
            )
        }
    }
}
@dFormCreate
class DFormTest extends Component {...}

此时DFormTest已经有了 getFieldDec。这个高阶修饰器就可以进一步处理表单元素:

让表单元素获得各种属性
代码语言:javascript复制
@dFormCreate
class DFormTest extends Component {

    render() {
        const { getFieldDec } = this.props;
        return (
            <div style={{ width: '60%', margin: 'auto' }}>
                {getFieldDec('username', {
                    rules: [{ required: true, message: 'Please input your username!' }],
                })(<input type="text" />)}

                {getFieldDec('password', {
                    rules: [{ required: true, message: 'Please input your password' }],
                })(<input type="password" />)}               
                <button>login</button>
            </div>
        );
    }
}
收集表单数据
代码语言:javascript复制
handleChange=(e)=>{
            const {name,value}=e.target;
            this.setState({
                [name]:value
            })
        }
添加校验(validateField)
代码语言:javascript复制
function dFormCreate(Component) {
    return class extends React.Component {
        constructor(props) {
            super(props)
            // 期望用户能给传配置选项
            this.options = {};
            this.state = {};
        }

        handleChange = (e) => {
            const { name, value } = e.target;

            this.setState({
                [name]: value
            }, () => {
                //单字段校验
                this.validateField(name);
            })
        }

        validateField = (field) => {
            //在此校验
            const rules = this.options[field].rules;

            // some里只要任何一项不通过,就不通过并跳出。
            const isValid = !rules.some(rule => {
                if (rule.required) {
                    if (!this.state[field]) {
                        // 校验失败
                        this.setState({
                            [field   'Message']: rule.message
                        })
                        return true;
                    }
                }
                return false;
            });

            if (!isValid) {
                this.setState(
                    { [field   'Message']: '' }
                )
            }

            return isValid;

        }

        //多个校验
        validateFields = (cb) => {
            // 将选项中所有field组成的数组转换为它们校验结果数组
            const rets = Object.keys(this.options).map((field) => {
                return this.validateField(field)
            })
            // 校验结果中每一项都要求true
            const ret = rets.every(v => v === true);
            console.log(222,this.state)
            cb(ret, this.state);
        }


        /**
         * 包装函数
         * 接收字段名/校验配置
         * 返回一个高阶组件
         */
        getFieldDec = (field, option) => {

            this.options[field] = option;//选项,告诉我们如何校验
            return InputComp => (
                <div>
                    {/* vdom不能修改,克隆一份再扩展 */}
                    {React.cloneElement(InputComp, {
                        name: field,
                        value: this.state[field] || '',
                        onChange: this.handleChange //执行校验,设置状态
                    })}
                </div>
            )
        }

        render() {

            return (
                <Component {...this.props} validateFields={this.validateFields} getFieldDec={this.getFieldDec} validate={this.validate} />
            )
        }
    }
}

0 人点赞