day-072-seventy-two-20230517-react组件化开发-小知识-创建一个React组件
react组件化开发
-
组件化开发
- 当下的前端开发,基本上都是
组件化+工程化
- 工程化
- grunt
- 一开始的打包工具。
- gulp
- 对grunt优化的打包工具。
- webpack
- 大概在2015年左右就是主流了。
- 目前2023依旧最主流。
- 命令化,插件化。
- 对于大项目已经有点慢了,比如冷启动要2分钟。
- vite(rollup)
- 冷启动及热更新是使用ES6模块引入的,分模块处理。速度比webpack快。
- 打包是rollup,目前的主要趋势。
- trubopack
- webpack官方出品,目前的趋势二。
- …
- grunt
- 组件化
- 把一个项目,划分成一个个的组件。
- 在项目上线时,会使用打包工具打包成一个或多个html文件。
- 组件分类:
- 依照开发过程中的作用:
- 业务组件:组件中集成了数据和业务逻辑。
- 普通业务组件:没有复用性。
- 如首页、商品页等。
- 通用业务组件:有复用性。
- 如猜你喜欢这一类的首页组件中的其中一块,在其它页面依旧能用的。
- 普通业务组件:没有复用性。
- 功能组件:不掺杂业务逻辑,适用性更强。
- UI组件库中提供的组件。
- 我们可能会对UI组件库中的组件进行二次封装。
- 结合项目需求和业务逻辑。
- 对于UI组件库中不存在的组件,我们需要自己封装。
- 例如:
- 大文件切片上传和断点续传。
- work与pdf预览和excel预览, 结合第三方插件。
- 地图类。
- 拖拽。
- 例如:
- 业务组件:组件中集成了数据和业务逻辑。
- 依照组件特征:
- 函数式组件
- 类组件
- 依照开发过程中的作用:
- 组件化开发的优势:
- 有利于代码的复用,提高开发效率,降低维护成本。
- 基本上,只要能复用一次,就封装。
- 一旦封装,后续如果用到,只要调用组件就可以了。
- 有利于团队的协作开发。
- 如每一个人都各自负责各自的组件,文件合并时不容易产生冲突。
- 有利于SPA单页面应用开发。
- 可以根据路由变化,切换或显示一些组件,以达到类似于页面切换的效果。
- 通过组件懒加载,还可以加快首屏显示速度。
- 通过webpack配置进行自定义划分区块,可以保证加载的东西不至于太大或太多。
- …
- 有利于代码的复用,提高开发效率,降低维护成本。
- 把一个项目,划分成一个个的组件。
- 工程化
- 当下的前端开发,基本上都是
-
React中的组件化开发
-
在Vue2中,其组件的划分有两大思路:
-
命名思路:
- kebab-case 调用,
<table-list/>
。 - PascalCase 创建,
TableList
。 - camalCase 非组件使用,
方法名
或属性名
。
- kebab-case 调用,
-
思路一:全局组件与局部组件,目前公司项目中划分的主要模式。
-
首先要创建一个 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也成长不了。
- 这个是因为组件文档的问题。UI框架类组件文档清晰,但项目内部的通用型组件,基本上没什么文档。当别人要维护或修一个bug时,看到四处引用的内部全局组件,基本上要崩溃,要文档没文档,也不好问同事,只能看组件源码,猜那个props有什么作用。不过,理论上,如果一个项目只有一两个人,那么这样搞最省事。而且,项目出问题,也只能找你,基本上不会被裁员。
- 个人更喜欢,因为除了UI框架类组件,当前vue组件内使用了那个内部,都可以找到。
-
-
思路二:
函数组件
与类组件
- 默认情况下,创建的单文件组件,都可以理解为是类组件。
- 每一次调用组件,都是创建这个类
VueComponent
的一个实例。- 也是Vue类的一个实例。
- 这样就可以基于
this.xxx
访问Vue.prototype
上的公共属性方法。
- 这样就可以基于
- 类组件中具备:属性、状态、计算属性、监听器、钩子函数…
- 也是Vue类的一个实例。
- 每一次调用组件,都是创建这个类
- 函数组件
<template funcational></template>
- 函数组件中只有:属性…
- 默认情况下,创建的单文件组件,都可以理解为是类组件。
-
-
在React中,其组件不分全局和局部的,或者说都是局部组件。
-
都得引入才能使用。
import VoteDemo from './views/VoteDemo'//@1 导入组件。 <VoteDemo/> //@2 直接调用。
-
-
React中的组件,分为以下几种:
- 函数组件。
- 纯函数组件。
- Hook组件:这个是2023年主流项目中主要使用的组件。
- 在函数组件中使用HooksAPI。
- 类组件。
- 函数组件。
-
-
React中的函数组件:
- 在Vue框架中,创建一个
.vue
文件,就是为了在该文件内部创建一个单文件组件。- 单文件组件包含:
- 视图。
- JavaScript。
- 样式。
- 全局样式。
- 组件内样式。
- 单文件组件包含:
- 在React框架中,创建一个
.jsx
文件,就是为了在该文件内部创建一个单文件组件。- 在项目中遇到有
.jsx后缀
的文件就表示是有React组件返回,以.js后缀
的就是没有React组件的。- 不这样写也没关系,以
.js后缀
的依旧可以创建组件,就是没提示功能。 - 以
.jsx后缀
的依旧可以不创建组件。 - 最好作区分。
- 按有返回一个React组件的为一个
.jsx后缀文件
。 - 按没有返回一个React组件的为一个
.js后缀文件
。
- 按有返回一个React组件的为一个
- 不这样写也没关系,以
- 一个单文件组件具有:
- 视图。
- JavaScript代码。
- 样式。
- 需要特殊处理。
- 如何创建一个函数组件
- 创建一个函数。
- 函数返回的是一个jsx元素
VirtualDOM
。
- 如何调用组件:
- 先导入。
- 再调用。
- React中的组件调用,需要基于
<Component>(PascalCase)这种模式
调用,不支持<kebab-case>模式
。- 因为组件命名时,就只能基于PascalCase这种方式。
- 单闭合和双闭合调用的唯一区别:双闭合调用可以传递子节点。
- 调用组件底层处理机制:
- 当我们调用组件的时候,首先基于
babel-preset-react-app
与React.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
-
…
-
- 基于
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。
-
- 当我们调用组件的时候,首先基于
- 在项目中遇到有
- 在Vue框架中,创建一个
-
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)
}
}
创建一个纯函数组件
-
如何创建一个函数组件
- 在合适的位置创建一个.jsx文件。
- 在.jsx文件中创建一个函数。
- 函数返回的是一个
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 = { … }
- 虽然不能直接修改props,但是我们可以对props进行规则校验。
- 在Vue框架中,我们可以基于slot和v-slot(简写为#)来处理插槽信息(而且有具名插槽和作用域插槽);React默认是不具备插槽这个概念的,但是真实项目中,插槽还是有用的!
- 插槽作用:把外界的一些视图,基于插槽传递到组件内部渲染,以此来增强组件的复用性!
- 具体处理办法:基于 props.children(存储了调用插槽时设置的子节点)。
- 如果组件只预留一个插槽,则直接在插槽位置,渲染 props.children 即可!
- 但是如果预留了很多插槽,则需要给插槽设置名字,后续调用的时候,指定名字,让其渲染到组件内部的特定位置。
- 我们可以把传递的 children 做特殊的处理。
- 我们可以基于 React.Children 中提供的方法来处理插槽信息。
- props是被冻结的「只读的」,如果需要修改,我们可以把属性值赋值给其他的变量/状态,以后基于修改变量/状态来改值。
/* 如何创建一个函数组件 + 创建一个函数 + 函数返回的是一个“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方法。
- 基于ES6中的class创建一个类。
-
当基于render方法渲染类组件的时候,会基于new创造类组件的一个实例,此时在React内部,会历经一系列的处理步骤 —> 组件第一次渲染。
- 也就是从new开始,到视图渲染完毕,会经历很多事件:
- 组件第一次渲染的逻辑。
- 初始化props与context。
- 接收传递的属性。
- 并且对属性进行规则校验 defaultProps/propTypes,静态私有属性。
- …
- 执行constructor函数,把处理好的props与context传递进行。
- 前提是设置了constructor这个函数。
- 各种信息的初始化处理。 或者说,就是把各种数据全部挂载到实例上。
- 属性:this.props
- 这个是只读的。
- 上下文:this.context
- ref操作:this.refs
- 更新队列:this.updater
- 状态:this.state
- 如果没有手动去初始化状态,则其默认值是null。
- 这个是最主要的,一般用于更新组件视图就是用它。
- 状态在组件中是很重要的,是我们在组件内自己构建的数据模型(Model层)。
- 我们可以按照需要随意更改状态数据,并且控制视图的渲染和更新。
- 属性:this.props
- 触发
- 触发render钩子函数执行
- 初始化props与context。
- 组件第一次渲染的逻辑。
- 也就是从new开始,到视图渲染完毕,会经历很多事件:
-
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
- 每一次调用这个组件,都是把函数执行「产生一个私有的上下文」;所以我即便调用多次这个函数组件,也是产生多个不同的私有上下文,保证每一次调用之间是互不影响的!
- 创建的是一个类组件
- 每一次调用类组件,都是创造类的一个实例,实例和实例之间也是独立的,确保多次调用之间互不影响!!
- 创建一个函数组件 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)
}
}