React(12)组合(类似 Vue 组件的插槽)和继承

x33g5p2x  于2022-03-06 转载在 其他  
字(5.2k)|赞(0)|评价(0)|浏览(616)

18、组合(类似 Vue 组件的插槽)

在Vue中,假如我们需要让子组件的一部分内容,被父组件控制,而不是被子组件控制,那么我们会采用插槽的写法 <slot></slot>

在 React 里也有类似的写法,父组件写法是相同的,但子组件是采用 {this.props.children} 来实现。

示例:

  1. class MyChild extends React.Component {
  2. render() {
  3. return <div>
  4. {this.props.children}
  5. </div>
  6. }
  7. }
  8. class WelcomeDialog extends React.Component {
  9. constructor(props) {
  10. super(props)
  11. this.state = {
  12. date: (new Date()).toLocaleTimeString()
  13. }
  14. setInterval(() => {
  15. // 显示内容全部由父组件控制,子组件不关心父组件显示什么,只关心显示在哪里
  16. this.setState({
  17. date: (new Date()).toLocaleTimeString()
  18. })
  19. }, 1000)
  20. }
  21. render() {
  22. return <div>
  23. <MyChild>
  24. {this.state.date}
  25. </MyChild>
  26. </div>
  27. }
  28. }

如上代码,子组件展现的内容,是父组件中,子组件标签内含的内容。

关于 this.props.children 这个变量:

<MyChild>为例,注意,这个名字只跟子组件名挂钩,不是固定的。

  1. 字符串:<MyChild>标签内是一个普通的字符串;
  2. 对 象:<MyChild>标签内是一个 DOM 元素,例如:<MyChild> <span>{this.state.date}</span> </MyChild>
  3. 数 组:<MyChild>标签内是多个 DOM 元素,例如:<MyChild> <span>{this.state.date}</span> <span>{this.state.date}</span> </MyChild>

如何获取 <MyChild> 标签内的二级或者更多级元素?

以以下为例:

  1. <MyChild>
  2. <div>
  3. abc
  4. <span>{this.state.date}</span>
  5. </div>
  6. </MyChild>
  1. 显然,标签内必须是一个DOM标签;
  2. DOM标签里有二级元素,以上为例,有两个元素,分别是一个字符串 abc 和一个 span 标签;
  3. 那么如何获取这两个元素呢?通过 this.props.children.props.children 来获取(第一个 children 指传递进来的元素,第二个指二级元素);
  4. this.props.children.props.children 在以上情况下,是一个数组,数组元素一是字符串 abc,数组元素二,是对象(即 span 标签);
  5. props 这个属性,不是固定类型的,可能是字符串,也可能是数组或对象,根据该级元素是什么,以及有几个而决定;

示例代码:

  1. class MyChild extends React.Component {
  2. constructor(props) {
  3. super(props)
  4. console.log(this.props)
  5. }
  6. render() {
  7. return <div>
  8. 父元素的第一个字符串:{this.props.children.props.children[0]}
  9. <br/>
  10. 父元素的第二个字符串:{this.props.children.props.children[1]}
  11. </div>
  12. }
  13. }
  14. class WelcomeDialog extends React.Component {
  15. constructor(props) {
  16. super(props)
  17. this.state = {
  18. date: (new Date()).toLocaleTimeString(),
  19. year: (new Date()).toLocaleDateString()
  20. }
  21. setInterval(() => {
  22. // 显示内容全部由父组件控制,子组件不关心父组件显示什么,只关心显示在哪里
  23. this.setState({
  24. date: (new Date()).toLocaleTimeString(),
  25. year: (new Date()).toLocaleDateString()
  26. })
  27. }, 1000)
  28. }
  29. render() {
  30. return <div>
  31. <MyChild>
  32. <div>
  33. {this.state.date}
  34. {this.state.year}
  35. </div>
  36. </MyChild>
  37. </div>
  38. }
  39. }

当然,如果是纯字符串的话,通过变量 props 直接传入更好一些。

而以上这种写法,更适合传 JSX 语法的 DOM 元素。

19、继承(事实上还是组合,但展现的效果像是继承)

我们有时候会面临这样一个情况:

  1. 有一个表单;
  2. 表单里有单行输入框(input)、多行输入框(textarea),也许还有其他的比如单选框,或多选框;
  3. 输入框的样式是统一的,所以希望统一管理;
  4. 但每个输入框的验证逻辑不同;
  5. 每个输入框都有自己的错误提示;

在往常情况下,我们是这么解决的:

  1. 将每个输入框单独拆分成组件,然后表单里依次引入这些组件;
  2. 对于 css,因为样式相同,所以采用的是统一的 class,因此可能需要将这些样式单独写入某个css文件,然后在这些样式组件里引入并使用这些类;
  3. 在每个组件里写样式,逻辑等;
  4. 如果多个样式他们有相同的逻辑,可能需要复制粘贴(优化方式是将这些验证逻辑单独抽象到一个js文件,然后在这些组件里引用这个js文件,并使用这些逻辑);

示例略。

这种写法,讲道理说,对于一般项目来说,也足够了,优化程度也不差;

但毕竟还有更好的写法,先提思路:

  1. 组件分为基础组件和扩展组件;
  2. 基础组件是输入框,有样式、HTML 元素、基本的验证逻辑、一些通用的逻辑等(比如通用错误提示);
  3. 扩展组件里内置基础组件,负责 控制值(传到基础组件,但是在这一层控制)、验证逻辑(显然姓名、电话、日期输入框,他们的验证逻辑是不同的)、提示信息(比如 HTML 标签的 title 属性)等;
  4. 表单里引入扩展组件;
  5. 这意味着,我们在使用的时候,只需要从扩展组件里引入基础组件即可;
  6. 在写扩展组件时,只需要专心于逻辑,不需要关心样式等通用性问题;
  7. 而写基础组件的时候,只需要关心共性,而不需要关心逻辑,有逻辑则使用逻辑,没有逻辑则执行空函数即可;

附代码(结尾见解释):

  1. // 基础组件
  2. class BaseInput extends React.Component {
  3. render() {
  4. let left = {display: 'inline-block', width: '100px'}
  5. let right = {display: 'inline-block', width: '200px', boxSizing: 'border-box'}
  6. let DOM
  7. let changeFn
  8. if (this.props.onChange) {
  9. changeFn = e => {
  10. this.props.onChange(e)
  11. }
  12. } else {
  13. changeFn = () => {
  14. }
  15. }
  16. // 首先,根据类型选择需要的输入框
  17. if (this.props.type === 'input' | !this.props.type) {
  18. DOM = <span>
  19. <span style={left}>{this.props.label}</span>
  20. <input style={right} type="text"
  21. onChange={changeFn}
  22. value={this.props.value}/>
  23. </span>
  24. } else if (this.props.type === 'textarea') {
  25. DOM = <span>
  26. <span style={left}>{this.props.label}</span>
  27. <textarea style={right} type="text" onChange={changeFn}
  28. value={this.props.value}/>
  29. </span>
  30. }
  31. return <div style={{height: '50px'}}>
  32. {DOM}
  33. {/* 其次,允许将额外补充内容添加到这里 */}
  34. {this.props.children}
  35. </div>
  36. }
  37. }
  38. class ChineseName extends React.Component {
  39. constructor(props) {
  40. super(props)
  41. this.state = {
  42. value: ''
  43. }
  44. this.verification = this.verification.bind(this)
  45. }
  46. verification(e) {
  47. let v = e.target.value
  48. // 必须是中文字符
  49. if (/[\u4e00-\u9fa5]/.test(v)) {
  50. this.setState({
  51. value: v
  52. })
  53. }
  54. }
  55. render() {
  56. return <BaseInput label={'名字'}
  57. type={'input'}
  58. value={this.state.value}
  59. onChange={this.verification}>
  60. <span style={{color: 'red'}}>只允许输入中文字符</span>
  61. </BaseInput>
  62. }
  63. }
  64. class EnglishName extends React.Component {
  65. constructor(props) {
  66. super(props)
  67. this.state = {
  68. value: ''
  69. }
  70. this.verification = this.verification.bind(this)
  71. }
  72. verification(e) {
  73. let v = e.target.value
  74. // 只能是英文字母和空白符
  75. if (!/[^a-zA-Z\s]/.test(v)) {
  76. this.setState({
  77. value: v
  78. })
  79. }
  80. }
  81. render() {
  82. return <BaseInput label={'英文名'}
  83. type={'input'}
  84. value={this.state.value}
  85. onChange={this.verification}>
  86. <span style={{color: 'red'}}>只允许输入a~z,或者A~Z,或者空白字符</span>
  87. </BaseInput>
  88. }
  89. }
  90. class TextareaInput extends React.Component {
  91. render() {
  92. return <BaseInput label={'个人情况'} type={'textarea'}>
  93. <span style={{color: 'red'}}>请务必填写</span>
  94. </BaseInput>
  95. }
  96. }
  97. ReactDOM.render(
  98. <div>
  99. <ChineseName/>
  100. <EnglishName/>
  101. <TextareaInput/>
  102. </div>,
  103. document.getElementById('root')
  104. )

说明:

  1. BaseInput 是基础组件,他负责开放接口。其他扩展组件只需要关心他开放了哪些接口,然后使用即可;
  2. ChineseName 是扩展组件之一(是基础组件的特殊实例);
  3. 通过 props.type 告诉基础组件,基础组件负责选择使用哪一个类型的 HTML 元素(input 或者 textarea);
  4. 通过 props.value 传值给基础组件,基础组件负责将这个指放到指定的位置;
  5. 传递验证函数 verification 给基础组件,基础组件负责管理什么时候调用他;
  6. 通过 props.children 告诉基础组件,需要将一些来源于扩展组件的 HTML 元素插入,而基础组件负责决定插入到哪里;

更好的优化方法:

  1. 假如这个基础组件更加复杂,可以将基础组件再拆分;
  2. 比如拆分为 MyInput 组件和 MyTextArea 组件;
  3. 基础组件专心于基础组件本身的样式,以及一些通用逻辑,以及 props.children 放置在哪里等;
  4. 细分后的 MyInput 组件,可以专心于搞定 input 输入框的样式,通用逻辑等;

相关文章