20230517----重返学习-react组件化开发-小知识-创建一个React组件

day-072-seventy-two-20230517-react组件化开发-小知识-创建一个React组件

react组件化开发

  1. 组件化开发

    • 当下的前端开发,基本上都是组件化+工程化
      • 工程化
        • grunt
          • 一开始的打包工具。
        • gulp
          • 对grunt优化的打包工具。
        • webpack
          • 大概在2015年左右就是主流了。
          • 目前2023依旧最主流。
          • 命令化,插件化。
          • 对于大项目已经有点慢了,比如冷启动要2分钟。
        • vite(rollup)
          • 冷启动及热更新是使用ES6模块引入的,分模块处理。速度比webpack快。
          • 打包是rollup,目前的主要趋势。
        • trubopack
          • webpack官方出品,目前的趋势二。
      • 组件化
        • 把一个项目,划分成一个个的组件。
          • 在项目上线时,会使用打包工具打包成一个或多个html文件。
        • 组件分类:
          • 依照开发过程中的作用:
            • 业务组件:组件中集成了数据和业务逻辑。
              • 普通业务组件:没有复用性。
                • 如首页、商品页等。
              • 通用业务组件:有复用性。
                • 如猜你喜欢这一类的首页组件中的其中一块,在其它页面依旧能用的。
            • 功能组件:不掺杂业务逻辑,适用性更强。
              • UI组件库中提供的组件。
              • 我们可能会对UI组件库中的组件进行二次封装。
                • 结合项目需求和业务逻辑。
              • 对于UI组件库中不存在的组件,我们需要自己封装。
                • 例如:
                  • 大文件切片上传和断点续传。
                  • work与pdf预览和excel预览, 结合第三方插件。
                  • 地图类。
                  • 拖拽。
          • 依照组件特征:
            • 函数式组件
            • 类组件
        • 组件化开发的优势:
          • 有利于代码的复用,提高开发效率,降低维护成本。
            • 基本上,只要能复用一次,就封装。
            • 一旦封装,后续如果用到,只要调用组件就可以了。
          • 有利于团队的协作开发。
            • 如每一个人都各自负责各自的组件,文件合并时不容易产生冲突。
          • 有利于SPA单页面应用开发。
            • 可以根据路由变化,切换或显示一些组件,以达到类似于页面切换的效果。
            • 通过组件懒加载,还可以加快首屏显示速度。
            • 通过webpack配置进行自定义划分区块,可以保证加载的东西不至于太大或太多。
  2. React中的组件化开发

    • 在Vue2中,其组件的划分有两大思路:

      • 命名思路:

        • kebab-case 调用,<table-list/>
        • PascalCase 创建,TableList
        • camalCase 非组件使用,方法名属性名
      • 思路一:全局组件与局部组件,目前公司项目中划分的主要模式。

        • 首先要创建一个 XxxXxx.vue 的单文件组件。

          • 假设组件名为VoteDemo;
        • 全局组件:

          Vue.component('XxxXxxx', 组件)
          
          • 这样在任何一个组件中,都可以直接调用这个组件!

            import VoteDemo from '...'
            Vue.component('VoteDemo', VoteDemo)
            
            • 基本上,功能组件都注册为全局组件。
              • 一般是复用超过多次如10次才注册。
              • 不过个人感觉引入更好,方便维护,能明确知道组件在那里定义。
        • 局部组件:

          <script>
          import VoteDemo from '...'  //@1 导入组件
          export default {
              components:{
                  VoteDemo  //@2 注册组件
              }
          }
          </script>
          <template>
            <vote-demo/>  //@3 调用组件
          </template>
          
          • 个人更喜欢,因为除了UI框架类组件,当前vue组件内使用了那个内部,都可以找到。
            • 这个是因为组件文档的问题。UI框架类组件文档清晰,但项目内部的通用型组件,基本上没什么文档。当别人要维护或修一个bug时,看到四处引用的内部全局组件,基本上要崩溃,要文档没文档,也不好问同事,只能看组件源码,猜那个props有什么作用。不过,理论上,如果一个项目只有一两个人,那么这样搞最省事。而且,项目出问题,也只能找你,基本上不会被裁员。
              • 没多少人喜欢背api,而且那api还不是通用的。UI框架的api都有文档,都有说明,大多数前端也不会没事去背!背api也成长不了。
      • 思路二:函数组件类组件

        • 默认情况下,创建的单文件组件,都可以理解为是类组件。
          • 每一次调用组件,都是创建这个类VueComponent的一个实例。
            • 也是Vue类的一个实例。
              • 这样就可以基于this.xxx访问Vue.prototype上的公共属性方法。
            • 类组件中具备:属性、状态、计算属性、监听器、钩子函数…
        • 函数组件 <template funcational></template>
          • 函数组件中只有:属性…
    • 在React中,其组件不分全局和局部的,或者说都是局部组件。

      • 都得引入才能使用。

        import VoteDemo from './views/VoteDemo'//@1 导入组件。
        <VoteDemo/> //@2 直接调用。
        
    • React中的组件,分为以下几种:

      • 函数组件。
        • 纯函数组件。
        • Hook组件:这个是2023年主流项目中主要使用的组件。
          • 在函数组件中使用HooksAPI。
      • 类组件。
  3. React中的函数组件:

    • 在Vue框架中,创建一个.vue文件,就是为了在该文件内部创建一个单文件组件。
      • 单文件组件包含:
        • 视图。
        • JavaScript。
        • 样式。
          • 全局样式。
          • 组件内样式。
    • 在React框架中,创建一个.jsx文件,就是为了在该文件内部创建一个单文件组件。
      • 在项目中遇到有.jsx后缀的文件就表示是有React组件返回,以.js后缀的就是没有React组件的。
        • 不这样写也没关系,以.js后缀的依旧可以创建组件,就是没提示功能。
        • .jsx后缀的依旧可以不创建组件。
        • 最好作区分。
          • 按有返回一个React组件的为一个.jsx后缀文件
          • 按没有返回一个React组件的为一个.js后缀文件
      • 一个单文件组件具有:
        • 视图。
        • JavaScript代码。
        • 样式。
          • 需要特殊处理。
      • 如何创建一个函数组件
        1. 创建一个函数。
        2. 函数返回的是一个jsx元素VirtualDOM
      • 如何调用组件:
        1. 先导入。
        2. 再调用。
        • React中的组件调用,需要基于<Component>(PascalCase)这种模式调用,不支持<kebab-case>模式
          • 因为组件命名时,就只能基于PascalCase这种方式。
        • 单闭合和双闭合调用的唯一区别:双闭合调用可以传递子节点。
        • 调用组件底层处理机制:
          1. 当我们调用组件的时候,首先基于babel-preset-react-appReact.createElement把其变为virtualDOM
            • type 普通函数/构造函数。

            • props 包含了调用组件时设置的属性。

              • 如果用双闭合方式调用,并且有子节点,则props中会新增一个children字段,存储子节点的信息。
                • 子节点信息可能是一个值或者一个数组
              • 如果是单闭合方式调用,那么就没有子节点。
              • 解析出来的props对象是只读的,实际上就是被冻结的。
                • 所以函数式组件是不能修改的,会报错。

                  let vd = React.createElement(
                    DemoOne,
                    {
                      title: "\u54C8\u54C8\u54C8",
                      x: 10
                    },
                    React.createElement("span", null, "\u563F\u563F\u563F")
                  )
                  console.log(Object.isFrozen(vd.props)) 
                  
            • key/ref

          2. 基于render()对virtualDOm进行渲染。
            • 如果type是一个普通函数,说明调用的是函数组件。

              • 把函数执行,把解析出来的props作为实参传递给函数。

              • 接收函数执行的返回值,一般是一个新的VirtualDOM,最后再把这个返回值进行渲染,渲染为真实DOM

                import React from 'react'
                import ReactDOM from 'react-dom/client'
                import DemoOne from './views/DemoOne'
                
                const root = ReactDOM.createRoot(document.getElementById('root'))
                root.render(
                  <>
                    <DemoOne title="哈哈哈" x="10">
                      <span name="slot2">嘿嘿嘿</span>
                      <span name="slot1">哇咔咔</span>
                    </DemoOne>
                  </>
                )
                
            • 如果type是一个构造函数,说明调用的是类组件,而且是继承了React.Compontent/PureCompontent的类组件。

              • 基于new创建类组件的一个实例,其内部要经过一套复杂的处理,并把解析出来的props也传递进去。
              • 调用实例的render方法,类组件就是在这个方法中构建需要的视图的,接收返回值。
                • 该返回值一般是一个新的VirtualDOM,最后基于render把其渲染为真实DOM。
  4. React.StrictMode标签

    • <React.StrictMode>标签可以开启内部被包裹React组件的严格模式。
      • 和JavaScript的严格模式并不一样,JS严格模式是为了让JS语法更加严谨。
      • React的严格模式是用来检测一些目前不建议使用的React老语法
        • 如果你使用了那些老语法,控制台就会抛出一些红色警告!
        • 例如:
          • 部分钩子函数 componentWillMount、componentWillUpdate、componentWillReceiveProps …
      • 一旦开启React组件的严格模式,React组件会被同时渲染两次,这点不是很好。
        • 但是可以在项目要上线时,在父组件上开启一下,以检测出那些语法不建议使用,并使用另外的方式去实现同样的效果。
          • 在上线前,把<React.StrictMode>标签移除。
        • 某些UI组件库中的组件,依然还在使用一些不被建议的老语法。
          • 所以我们平时开发中,是不开启严格模式的!
            • 因为这会导致项目中四处报错,真正需要修改的错误不容易被发现。
    const root = ReactDOM.createRoot(document.getElementById('root'))
    root.render(
      <React.StrictMode>
        <DemoOne title="哈哈哈" x="10">
          <span name="slot2">嘿嘿嘿</span>
          <span name="slot1">哇咔咔</span>
        </DemoOne>
      </React.StrictMode>
    )
    

小知识

项目版本号

  • 查看项目版本号

    npm  view xxx versions
    
    • 得到项目版本号列表
  • 一个项目版本号列表大概为:

    • 版本号-版本后缀.后缀阶段数字

      • 版本号分为主版本号.副版本号.补丁包版本号
        • 主版本号
        • 副版本号
        • 补丁包版本号
      • 版本后缀一般有以下几个词。
        • alpha 内测版。
        • beta 公测版。
        • rc 预发版。
        • stable 正式版或稳定版。
        • 无后缀版 正式实用版,基本上后续就只发补丁或改版本号了。
      • 后缀阶段数字 数字依次递增,数值越大,表示越后发布,越新。
      版本号-版本后缀
      3.0.2
      主版本号.副版本号.补丁包
      
      3.0.2-版本后缀
      后缀的意思:
        alpha 内测
        beta 公测
        rc 预发
        stable 正式版(稳定版)
        没有后缀版
      
  • 一个完整的版本号:

    • 主版本号.副版本号.补丁包版本号-版本后缀.后缀阶段数字
    • 3.2.0-beta.7 表示:主版本号为3,副版本号为2,补丁包版本为0,处于公测第7轮次的项目版本

ECMAScript语言标准

  • 语言版本
    • 但是对于技术语言规范,发布之前也有几个阶段
      • stage-0
      • stage-1
      • stage-2
      • stage-3
        • 草案阶段「一般能够活到这个阶段的,在正式发版中都会有」
      • 正式发布 但不代表这个就是正式的了,得要各浏览器兼容。之后如果过了一段时间,还可能要被废弃。

创建一个React组件

// render:把虚拟DOM变为真实DOM
export const render = function render(virtualDOM, container) {
    let { type, props } = virtualDOM
    // 创建元素标签(真实DOM)
    if (typeof type === 'string') {
        let elem = document.createElement(type)
        //....
        container.appendChild(elem)
    }
    // 这是一个组件
    if (typeof type === 'function') {
        // 如果是类组件
        if (type.prototype.isReactComponent) {
            let inst = new type(props)
            //....
            let newVirtualDOM = inst.render()
            render(newVirtualDOM, container)
            return
        }
        // 如果是函数组件
        let newVirtualDOM = type(props)
        render(newVirtualDOM, container)
    }
}

创建一个纯函数组件

  • 如何创建一个函数组件

    1. 在合适的位置创建一个.jsx文件。
    2. 在.jsx文件中创建一个函数。
    3. 函数返回的是一个JSX元素(VirtualDOM)
      • 函数中可以接收一个值props,一个在React.createElement()调用该函数会传递进来的参数。
      • props接收传递进来的属性(含子节点)
        • props是被冻结的「只读的」,如果需要修改,我们可以把属性值赋值给其他的变量/状态,以后基于修改变量/状态来改值。
          • 虽然不能直接修改props,但是我们可以对props进行规则校验。
            • 属性规则设置:

              依赖官方插件 prop-types 「$ yarn add prop-types」
              https://github.com/facebook/prop-types
              组件.propTypes = {
                ...
              }
              
              • 备注:props中的信息是在创建VirtualDOM的时候就获取到了,而规则校验是在函数执行的时候,把传递进来的props中的每一项,按照规则进行校验的,所以即便不符合校验规则,也仅仅是在控制台输出Warning警告错误,但是不影响props中的值!
          • 设置默认值:组件.defaultProps = { … }
        • 在Vue框架中,我们可以基于slot和v-slot(简写为#)来处理插槽信息(而且有具名插槽和作用域插槽);React默认是不具备插槽这个概念的,但是真实项目中,插槽还是有用的!
          • 插槽作用:把外界的一些视图,基于插槽传递到组件内部渲染,以此来增强组件的复用性!
          • 具体处理办法:基于 props.children(存储了调用插槽时设置的子节点)。
            • 如果组件只预留一个插槽,则直接在插槽位置,渲染 props.children 即可!
            • 但是如果预留了很多插槽,则需要给插槽设置名字,后续调用的时候,指定名字,让其渲染到组件内部的特定位置。
          • 我们可以把传递的 children 做特殊的处理。
          • 我们可以基于 React.Children 中提供的方法来处理插槽信息。
    /* 
    如何创建一个函数组件
      + 创建一个函数
      + 函数返回的是一个“JSX元素(VirtualDOM)”
    
    props接收传递进来的属性(含子节点)
      + 被冻结的「只读的」,如果需要修改,我们可以把属性值赋值给其他的变量/状态,以后基于修改变量/状态来改值
      + 虽然不能直接修改props,但是我们可以对props进行规则校验
        设置默认值:组件.defaultProps = { ... }
        属性规则设置:
          依赖官方插件 prop-types 「$ yarn add prop-types」
          https://github.com/facebook/prop-types
          组件.propTypes = {
            ...
          }
          备注:props中的信息是在创建VirtualDOM的时候就获取到了,而规则校验是在函数执行的时候,把传递进来的props中的每一项,按照规则进行校验的,所以即便不符合校验规则,也仅仅是在控制台输出Warning警告错误,但是不影响props中的值!
    
    在Vue框架中,我们可以基于 <slot> 和 v-slot(简写#) 来处理插槽信息(而且有具名插槽和作用域插槽);React默认是不具备插槽这个概念的,但是真实项目中,插槽还是有用的!
      插槽作用:把外界的一些视图,基于插槽传递到组件内部渲染,以此来增强组件的复用性!
      具体处理办法:基于 props.children(存储了调用插槽时设置的子节点)
        + 如果组件只预留一个插槽,则直接在插槽位置,渲染 props.children 即可!
        + 但是如果预留了很多插槽,则需要给插槽设置名字,后续调用的时候,指定名字,让其渲染到组件内部的特定位置
      我们可以把传递的 children 做特殊的处理
      我们可以基于 React.Children 中提供的方法来处理插槽信息
    */
    import React from 'react'
    import PT from 'prop-types'
    
    const DemoOne = function DemoOne(props) {
        let x = props.x,
            children = React.Children.toArray(props.children),
            header = [],
            footer = []
        x = 1000
        children.forEach(item => {
            if (item.props.name === 'slot1') {
                header.push(item)
            }
            if (item.props.name === 'slot2') {
                footer.push(item)
            }
        })
    
        return <div className="demo-box">
            {header}
            <br />
            纯函数组件 {props.title} && {x}
            <br />
            {footer}
        </div>
    }
    // 设置传递属性的默认值
    DemoOne.defaultProps = {
        x: 0,
        y: false
    }
    // 设置其它的规则「必传、类型...」
    DemoOne.propTypes = {
        title: PT.string.isRequired, //字符串类型 & 必须传
        x: PT.oneOfType([  //多类型
            PT.number,
            PT.string
        ]),
        // y: PT.bool,
        customProp(props) { //自定义校验规则
            if (typeof props.y !== 'boolean') {
                return new Error(`y is not a boolean`)
            }
        }
    }
    export default DemoOne
    

纯函数组件的特点

  • 纯函数组件的特点:
    • 它是一个静态组件:第一次渲染组件后,无法基于组件内部的某些操作,让组件再次更新。

      • 暂时没有,后续基于HookAPI可以实现。
      • 或许也可以通过闭包,返回一个闭包函数,闭包内绑定的方法可以调用该闭包函数,进而返回不同的值。
        • 还可以尝试在函数第一次运行时,把this绑定下来,存到该闭包函数静态属性上。
        • 或者把闭包函数变成响应式,或内部变成响应式,或仿HookAPI的原理。
        • 这些思路没具体试过,不确定能否成功。目前没看到具体的简单就能改的。
    • 想让函数组件再次更新,只能重新调用该函数,并且传递新的属性值进来,以完成不一样的初始化。

      • 换句话说,函数组件的每一次渲染和更新,都需要把函数重新执行。
        • index.jsx

          import DemoOne from "./views/DemoOne";
          const root = ReactDOM.createRoot(document.getElementById("root"));
          root.render(
            <>
              <DemoOne title="哈哈哈" x="10">
                <span name="slot2">嘿嘿嘿</span>
                <span name="slot1">哇咔咔</span>
              </DemoOne>
            </>
          );
          setTimeout(() => {
            root.render(
              <>
                <DemoOne title="嘿嘿嘿" x="100"></DemoOne>
              </>
            );
          }, 2000);
          
        • DemoOne.jsx

          import React from 'react'
          import PT from 'prop-types'
          
          const DemoOne = function DemoOne(props) {
              let x = props.x,
                  children = React.Children.toArray(props.children),
                  header = [],
                  footer = []
              // x = 1000
              children.forEach(item => {
                  if (item.props.name === 'slot1') {
                      header.push(item)
                  }
                  if (item.props.name === 'slot2') {
                      footer.push(item)
                  }
              })
          
              return <div className="demo-box"
                  onClick={() => {
                      x = 2000
                      console.log(x)
                  }}>
                  {header}
                  <br />
                  纯函数组件 {props.title} && {x}
                  <br />
                  {footer}
              </div>
          }
          // 设置传递属性的默认值
          DemoOne.defaultProps = {
              x: 0,
              y: false
          }
          // 设置其它的规则「必传、类型...」
          DemoOne.propTypes = {
              title: PT.string.isRequired, //字符串类型 & 必须传
              x: PT.oneOfType([  //多类型
                  PT.number,
                  PT.string
              ]),
              // y: PT.bool,
              customProp(props) { //自定义校验规则
                  if (typeof props.y !== 'boolean') {
                      return new Error(`y is not a boolean`)
                  }
              }
          }
          export default DemoOne
          
    • 函数组件中有props属性,但是不具备:状态、钩子函数等内容。

    • 函数组件有一个好处:渲染速度快,不需要像类组件一样,处理很多事件。

      • 所以真实真实项目中,只需要渲染一次即可的需求,可以使用函数组件!

创建一个类组件

  • 如何创建一个类组件

    • 基于ES6中的class创建一个类。
      • 但是这个新创建的类必须继承 react中的Component或者PureComponent类。

        import {Component,PureComponent} from 'react'
        
        class DemoTwo extends Component{
          
        }
        
      • 继承的目的

        • 假设子类为DemoTwo,父类为Component。
        • 让子类的实例,具有子类提供的私有属性,以及子类原型上的公共方法。
        • 最主要的是,还需要让子类的实例,继承父类提供的私有属性,以及父类原型上的公共方法。
          • 私有属性:
          • 公共方法:
    • 而且必须在子类的原型对象上设置 render() 函数,让其返回需要构建的视图virtualDOM;
      • 这个新创建的类的原型上必须要有render方法。
  • 当基于render方法渲染类组件的时候,会基于new创造类组件的一个实例,此时在React内部,会历经一系列的处理步骤 —> 组件第一次渲染。

    • 也就是从new开始,到视图渲染完毕,会经历很多事件:
      1. 组件第一次渲染的逻辑。
        1. 初始化props与context。
          1. 接收传递的属性。
          2. 并且对属性进行规则校验 defaultProps/propTypes,静态私有属性。
        2. 执行constructor函数,把处理好的props与context传递进行。
          • 前提是设置了constructor这个函数。
        3. 各种信息的初始化处理。 或者说,就是把各种数据全部挂载到实例上。
          • 属性:this.props
            • 这个是只读的。
          • 上下文:this.context
          • ref操作:this.refs
          • 更新队列:this.updater
          • 状态:this.state
            • 如果没有手动去初始化状态,则其默认值是null。
            • 这个是最主要的,一般用于更新组件视图就是用它。
            • 状态在组件中是很重要的,是我们在组件内自己构建的数据模型(Model层)。
              • 我们可以按照需要随意更改状态数据,并且控制视图的渲染和更新。
        4. 触发
        5. 触发render钩子函数执行
  • class 语法

    class Xxx {
        // 给实例设置私有的
        constructor(x) {
            this.x = x
        }
        y = 20 //this.y=20
        fn = () => { } //this.fn=()=>{}
    
        // 给实例设置公有的「设置类原型对象上的」
        say() { }
        write() { }
    
        // 把其作为普通对象,设置属性和方法「和实例是没关系的」
        static n = 10  // Xxx.n
        static setN() { } // Xxx.setN()
    }
    Xxx.prototype.AAA = 100
    

纯函数组件与类函数的关系

  • 组件的创建

    • 创建一个函数组件 DemoFun
      • 每一次调用这个组件,都是把函数执行「产生一个私有的上下文」;所以我即便调用多次这个函数组件,也是产生多个不同的私有上下文,保证每一次调用之间是互不影响的!
    • 创建的是一个类组件
      • 每一次调用类组件,都是创造类的一个实例,实例和实例之间也是独立的,确保多次调用之间互不影响!!
  • 组件的更新

    • 函数组件的更新,依赖的是函数的重新执行(产生全新的私有上下文)
    • 类组件的更新,不是重新创建新的实例,而是把 实例.render 方法重新执行
/* render:把虚拟DOM变为真实DOM */
export const render = function render(virtualDOM, container) {
    let { type, props } = virtualDOM
    // 创建元素标签(真实DOM)
    if (typeof type === 'string') {
        let elem = document.createElement(type)
        Object.keys(props).forEach(key => {
            let value = props[key]
            if (key === 'className') {
                elem.setAttribute('class', value)
                return
            }
            if (key === 'style') {
                // value:样式对象,我们需要分别赋值元素的 style 行内样式
                _.each(value, (styV, styK) => {
                    elem.style[styK] = styV
                })
                return
            }
            if (key === 'children') {
                // 处理元素的子节点,value 是children属性的值
                let children = !Array.isArray(value) ? [value] : value
                children.forEach(child => {
                    // child:每个子节点「文本|元素」
                    if (typeof child === 'string') {
                        // 文本子节点
                        let textNode = document.createTextNode(child)
                        elem.appendChild(textNode)
                        return
                    }
                    // 元素子节点:递归处理
                    render(child, elem)
                })
                return
            }
            elem.setAttribute(key, value)
        })
        container.appendChild(elem)
    }
    // 这是一个组件
    if (typeof type === 'function') {
        // 如果是类组件
        if (type.prototype.isReactComponent) {
            let inst = new type(props)
            //....
            let newVirtualDOM = inst.render()
            render(newVirtualDOM, container)
            return
        }
        // 如果是函数组件
        let newVirtualDOM = type(props)
        render(newVirtualDOM, container)
    }
}

进阶参考

  1. prop-types规则-官方
  2. prop-types规则-npm
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值