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

生来就可爱ヽ(ⅴ<●) 提交于 2020-12-06 03:12:33



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

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


本文知识要点

  • Hook

  • 高阶组件

  • 组件通信

  • 上下文

  • React.cloneElement

Hook

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

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

它具有如下特点:

  • 在无需修改状态的情况下,复用状态逻辑。

  • 将相关联的部分拆分为更小的函数,复杂组件将更容易理解。

  • 更简洁,更易理解。

状态钩子 State Hook

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

  
    
  
  
  1. function Example() {

  2. // 声明一个新的叫做 “count” 的 state 变量,

  3. // 数组第二个值是变更函数,参数接收的是新状态

  4. // useState参数是初始值。

  5. const [count, setCount] = useState(0);


  6. return (

  7. <div>

  8. <p>You clicked {count} times</p>

  9. <button onClick={() => setCount(count + 1)}>

  10. Click me

  11. </button>

  12. </div>

  13. );

  14. }

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

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

  
    
  
  
  1. // 添加水果

  2. function AddFruit({setFruits,fruits}){


  3. return (

  4. <input type='text' onKeyUp={(e)=>{

  5. if(e.keyCode==13){

  6. setFruits(fruits.concat(e.target.value))

  7. e.target.value='';

  8. }

  9. e.persist()

  10. }}/>

  11. )

  12. }


  13. export default function HooksTest() {

  14. const [fruit, setFruit] = useState("草莓");

  15. const [fruits, setFruits ] = useState(['草莓','苹果','鸭梨']);


  16. return (

  17. <div>

  18. <p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>

  19. <AddFruit setFruits={setFruits} fruits={fruits}></AddFruit>

  20. <FruitList setFruit={setFruit} fruits={fruits}></FruitList>

  21. </div>

  22. );

  23. }

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

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

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

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

基本使用

  
    
  
  
  1. import { useEffect } from "react";


  2. export default function HooksTest() {

  3. const [fruit, setFruit] = useState("草莓");

  4. const [fruits, setFruits] = useState([]);

  5. // 使用useEffect异步获取数据

  6. useEffect(() => {

  7. setTimeout(() => {

  8. setFruits(['香蕉', '苹果'])

  9. }, 1000);

  10. })

  11. return (

  12. <div>

  13. <p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>

  14. <AddFruit setFruits={setFruits} fruits={fruits}></AddFruit>

  15. <FruitList setFruit={setFruit} fruits={fruits}></FruitList>

  16. </div>

  17. );

  18. }

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

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

  
    
  
  
  1. export default function HooksTest() {

  2. const [fruit, setFruit] = useState("草莓");

  3. const [fruits, setFruits] = useState(['草莓','香蕉']);

  4. // 使用useEffect异步获取数据

  5. useEffect(() => {

  6. document.title=fruit;

  7. },[fruit])

  8. return (

  9. <div>

  10. <p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>

  11. <AddFruit setFruits={setFruits} fruits={fruits}></AddFruit>

  12. <FruitList setFruit={setFruit} fruits={fruits}></FruitList>

  13. </div>

  14. );

  15. }

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

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

清除依赖:

  
    
  
  
  1. useEffect(()=>{...}, [])


  2. useEffect(() => {

  3. const timer = setInterval(() => {

  4. console.log('msg');

  5. }, 1000);

  6. return function(){

  7. clearInterval(timer);

  8. } }, []);

useReducer (状态管理lowb实现)

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

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

  
    
  
  
  1. import { useReducer } from "react";

  2. // 状态维护reducer

  3. function fruitReducer(state, action) {

  4. switch (action.type) {

  5. case "init":

  6. return action.payload;

  7. case "add":

  8. return [...state, action.payload];

  9. default:

  10. return state;

  11. }

  12. }


  13. // 添加水果

  14. function AddFruit({ onAddFruit, fruits }) {


  15. return (

  16. <input type='text' onKeyUp={(e) => {

  17. if (e.keyCode == 13) {

  18. onAddFruit(e.target.value)

  19. e.target.value = '';

  20. }

  21. e.persist()

  22. }} />

  23. )

  24. }


  25. export default function HooksTest() {

  26. const [fruits, dispatch] = useReducer(fruitReducer, []);

  27. const [fruit, setFruit] = useState("草莓");

  28. useEffect(() => {

  29. setTimeout(() => {

  30. // 变更状态,提交

  31. dispatch({ type: "init", payload: ["香蕉", "西瓜"] });

  32. }, 1000);

  33. }, []);

  34. return (

  35. <div>

  36. {/*变更状态*/}

  37. <AddFruit onAddFruit={pname => dispatch({ type: 'add', payload: pname })} fruits={fruits}></AddFruit>

  38. <FruitList setFruit={setFruit} fruits={fruits}></FruitList>

  39. </div>

  40. );

  41. }

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

useContext

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

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

  
    
  
  
  1. import React, { useContext } from "react"; // 创建上下文

  2. const Context = React.createContext();

  3. export default function HooksTest() {

  4. // ...

  5. return (

  6. {/* 提供上下文的值 */ }

  7. < Context.Provider value = {{ fruits, dispatch }}>

  8. <div>

  9. {/* 这里不再需要给FruitAdd传递变更函数,实现了解耦 */}

  10. <AddFruit />

  11. </div>

  12. </Context.Provider >

  13. ); }

  14. function AddFruit(props) {

  15. // 获取上下文

  16. const { dispatch } = useContext(Context)

  17. const onAddFruit = e => {

  18. if (e.key === "Enter") {

  19. // 直接派发动作修改状态

  20. dispatch({ type: "add", payload: pname }) setPname("");

  21. }

  22. };

  23. // ...

  24. }

清爽多了!

不过对于傻瓜组件,可以不考虑接耦。也不见得这种方法完全取代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;



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

  • 数据收集

  • 校验

  • 提交

表单的结构如下

  
    
  
  
  1. | - Form

  2. |-FormItem

  3. |-校验规则渲染下的表单组件

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

  
    
  
  
  1. const { getFieldDecorator } = this.props.form;

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

倒数第二行:

  
    
  
  
  1. const WrappedNormalLoginForm = Form.create({ name: 'normal_login' })(NormalLoginForm);

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

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

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

  
    
  
  
  1. handleSubmit = e => {

  2. e.preventDefault();

  3. this.props.form.validateFields((err, values) => {

  4. if (!err) {

  5. console.log('Received values of form: ', values);

  6. }

  7. });

  8. };

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

造轮子第一步

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

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

  
    
  
  
  1. import React, { Component } from "react";




  2. export default class DFormTest extends Component {

  3. render() {

  4. return (

  5. <div>

  6. <input type="text" />

  7. <input type="password" />

  8. <button>登录</Button>

  9. </div>

  10. );

  11. }

  12. }

那么就有一个样子了。

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

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


  • 控件包装



  • 时间处理



  • 事件处理


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

  
    
  
  
  1. function dFormCreate(Component){

  2. return class extends React.Component{

  3. constructor(props){

  4. super(props)

  5. // 期望用户能给传配置选项

  6. this.option={};

  7. this.state={};

  8. }

  9. /**

  10. * 包装函数

  11. * 接收字段名/校验配置

  12. * 返回一个高阶组件

  13. */

  14. getFieldDec=(field,option)=>{

  15. this.option[field]=option;//选项告诉我们如何校验

  16. return InputComp =>(

  17. <div>

  18. {/* vdom不能修改,克隆一份再扩展 */}

  19. {React.cloneElement(InputComp,{

  20. name:field,

  21. value:this.state[field]||'',

  22. onChange:this.handleChange //执行校验,设置状态

  23. })}

  24. </div>

  25. )

  26. }


  27. render(){


  28. return (

  29. <Component {...this.props} getFieldDec={this.getFieldDec}/>

  30. )

  31. }

  32. }

  33. }

  34. @dFormCreate

  35. class DFormTest extends Component {...}

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

让表单元素获得各种属性

  
    
  
  
  1. @dFormCreate

  2. class DFormTest extends Component {


  3. render() {

  4. const { getFieldDec } = this.props;

  5. return (

  6. <div style={{ width: '60%', margin: 'auto' }}>

  7. {getFieldDec('username', {

  8. rules: [{ required: true, message: 'Please input your username!' }],

  9. })(<input type="text" />)}


  10. {getFieldDec('password', {

  11. rules: [{ required: true, message: 'Please input your password' }],

  12. })(<input type="password" />)}

  13. <button>login</button>

  14. </div>

  15. );

  16. }

  17. }

收集表单数据

  
    
  
  
  1. handleChange=(e)=>{

  2. const {name,value}=e.target;

  3. this.setState({

  4. [name]:value

  5. })

  6. }

添加校验(validateField)

  
    
  
  
  1. function dFormCreate(Component) {

  2. return class extends React.Component {

  3. constructor(props) {

  4. super(props)

  5. // 期望用户能给传配置选项

  6. this.options = {};

  7. this.state = {};

  8. }


  9. handleChange = (e) => {

  10. const { name, value } = e.target;


  11. this.setState({

  12. [name]: value

  13. }, () => {

  14. //单字段校验

  15. this.validateField(name);

  16. })

  17. }


  18. validateField = (field) => {

  19. //在此校验

  20. const rules = this.options[field].rules;


  21. // some里只要任何一项不通过,就不通过并跳出。

  22. const isValid = !rules.some(rule => {

  23. if (rule.required) {

  24. if (!this.state[field]) {

  25. // 校验失败

  26. this.setState({

  27. [field + 'Message']: rule.message

  28. })

  29. return true;

  30. }

  31. }

  32. return false;

  33. });


  34. if (!isValid) {

  35. this.setState(

  36. { [field + 'Message']: '' }

  37. )

  38. }


  39. return isValid;


  40. }


  41. //多个校验

  42. validateFields = (cb) => {

  43. // 将选项中所有field组成的数组转换为它们校验结果数组

  44. const rets = Object.keys(this.options).map((field) => {

  45. return this.validateField(field)

  46. })

  47. // 校验结果中每一项都要求true

  48. const ret = rets.every(v => v === true);

  49. console.log(222,this.state)

  50. cb(ret, this.state);

  51. }



  52. /**

  53. * 包装函数

  54. * 接收字段名/校验配置

  55. * 返回一个高阶组件

  56. */

  57. getFieldDec = (field, option) => {


  58. this.options[field] = option;//选项,告诉我们如何校验

  59. return InputComp => (

  60. <div>

  61. {/* vdom不能修改,克隆一份再扩展 */}

  62. {React.cloneElement(InputComp, {

  63. name: field,

  64. value: this.state[field] || '',

  65. onChange: this.handleChange //执行校验,设置状态

  66. })}

  67. </div>

  68. )

  69. }


  70. render() {


  71. return (

  72. <Component {...this.props} validateFields={this.validateFields} getFieldDec={this.getFieldDec} validate={this.validate} />

  73. )

  74. }

  75. }

  76. }


本文分享自微信公众号 - 一Li小麦(gh_c88159ec1309)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!