组件的使用、开发、发布和规范
Component,中文称为组件,或者构件。使用非常比较广泛,它的核心意义在于复用,相对模块,对于依赖性有更高的要求。
Module, 中文为模块或模组。它的核心意义是分离职责,属于代码级模块化的产出。它本身是提供服务的功能逻辑,是一组具有一定内聚性代码的组合,职责明确。
Component 包含两类一类是通用组件,一类是业务组件。通用组件是一种只支持复用、且相对于模板不同的是其无任何业务逻辑或者说是无任何请求方法的组件,一般情况下通用组建构建完成后需要上传至组建库维护。而业务组件则不同,业务组件是一种使用模板、对功能做的一个集中化处理、具有一定的业务逻辑、请求方法内置等功能性组建,这种组件一般只是用来在程序内部组件·库使用无需上传至公用组件库。
前端Web应用中的组件,是指一些设计为通用性的,用来构建较大型应用程序的软件,这些组件有多种表现形式。它可以是有UI(用户界面)的,也可以是作为 “服务”的纯逻辑代码。因为有视觉上的表现形式,UI组件更容易理解。UI组件简单的例子包括按钮、输入框和文本域。不论是汉堡包状的菜单按钮(无论你是否喜欢)、标签页、日历、选项菜单或者所见即所得的富文本编辑器则是一些更加高级的例子。提供服务类型的组件可能会让人难以理解,这种类型的例子包括跨浏览器的AJAX支持,日志记录或者提供某种数据持久化的功能。
基于组件开发,最重要的就是组件可以用来构成其他组件,而富文本编辑器就是个很好的例子。它是由按钮、下拉菜单和一些可视化组件等组成。
为什么要构建组件?
高内聚
我们将相关的一些功能组织在一起,把一切封装起来,而在组件的例子中,就可能是相关的功能逻辑和静态资源:JavaScript、HTML、CSS以及图像等。这就是我们所说的内聚。这种做法将让组件更容易维护,并且这么做之后,组件的可靠性也将提高。同时,它也能让组件的功能明确,增大组件重用的可能性。
可重用
你看到的示例组件,尤其是Web Component,更关心可重用的问题。功能明确,实现清晰,API易于理解。自然就能促进组件复用。通过构建可重用组件,我们不仅保持了 DRY(不要重复造轮子)原则,还得到了相应的好处。
不要过分尝试构建可重用组件。你更应该关注应用程序上所需要的那些特定部分。如果之后相应需求出现,或者组件的确到了可重用的地步,就花一点额外时间让组件重用。事实上,开发者都喜欢去创造可重用功能块(库、组件、模块、插件等),做得太早将会让你后来痛苦不堪。所以,吸取基于组件开发的其他好处,并且接受不是所有组件都能重用的事实。
可互换
一个功能明确好组件的API能让人轻易地更改其内部的功能实现。要是程序内部的组件是松耦合的,那事实上可以用一个组件轻易地替换另一个组件,只要遵循相同的 API/接口/约定。
可组合
基于组件的架构让组件组合成新组件更加容易。这样的设计让组件更加专注,也让其他组件中构建和暴露的功能更好利用。不论是给程序添加功能,还是用来制作完整的程序,更加复杂的功能也能如法炮制。这就是这种方法的主要好处。
组件的开发
1.组件需求
组件名称:列表信息组件(SYCardInfo)
实现功能:主要提供统一列表信息显示、功能性事件、统一样式、可编辑样式等
页面demo:
/* 原始页面代码 */
import React, { Component } from 'react'
import { Icon } from 'antd'
import Style from './style.module.less'
class Index extends Component {
state={
showLine: true,
stripVisible: true,
showBtn: true
}
// 渲染方法的重写
render() {
let { showLine, stripVisible, showBtn } = this.state
return (
<>
<div className={Style.contentHeaderDiv}>
<div
className={showLine ? Style.contentHeader : Style.contentHeaderNoBorder}
style={{ padding: this.props.padding }}
>
<div className={Style.leftStyle}>
{ stripVisible && <div style={{ marginRight: '6px', width: '4px', height: '16px', borderRadius: '1px', backgroundColor: '#4A90E2', display: 'inline' }}> </div>}
<span>title</span>
</div>
<div className={Style.rightStyle}>
<span>this is onClike text!</span>
<Icon type='right' style={{ color: '#4A90E2' }} />
</div>
</div>
<div className={showLine ? Style.contentHeader : Style.contentHeaderNoBorder}
style={{ padding: this.props.padding }}
>
this is card info!
</div>
<div className={showBtn ? Style.footerBtn : null}>
<div className={Style.rightStyleText}>
Button1
</div>
<div className={Style.rightStyleIcon}>
Button2
</div>
</div>
</div>
</>
)
}
}
export default Index
2.实现过程
-
创建组件文件目录、创建组件文件夹、创建组件Jsx文件使用驼峰式命名规范进行
|------- SYCardInfo 文件夹
|---------- index.js 组件默认读取文件
|---------- index.mdx 组件说明文档
|---------- style.module.less 组件less表
-
导出新添加组件在index.js
import SYCardInfo from './components/SYCardInfo' export { SYCardInfo }
-
提取公共部分程序代码,进行初次封装
<div className={Style.contentHeaderDiv}> <div className={showLine ? Style.contentHeader : Style.contentHeaderNoBorder} style={{ padding: this.props.padding }} > <div className={Style.leftStyle}> { stripVisible && <div style={{ marginRight: '6px', width: '4px', height: '16px', borderRadius: '1px', backgroundColor: '#4A90E2', display: 'inline' }}> </div>} <span>title</span> </div> <div className={Style.rightStyle}> <span>this is onClike text!</span> <Icon type='right' style={{ color: '#4A90E2' }} /> </div> </div> <div className={showLine ? Style.contentHeader : Style.contentHeaderNoBorder} style={{ padding: this.props.padding }} > this is card info! </div> <div className={showBtn ? Style.footerBtn : null}> <div className={Style.rightStyleText}> Button1 </div> <div className={Style.rightStyleIcon}> Button2 </div> </div> </div>
-
编写组件文件
-
引入react依赖文件
import React, { Component } from 'react' import Style from './style.module.less' import propTypes from 'prop-types'
-
以类组件形式编写组件文件
class class Index extends Component { static defaultProps={} static propTypes={} state = {} render(){ <></> } }
-
提取公共使用代码程序进行改造
在提取改造代码的时候需要把动态的参数、方法、样式等需要有的变化做一个抽象化
render() { let { showLine, stripVisible, showBtn, disContent, disFooter } = this.state let { leftColor, btnOkStyle, padding, title, rightContent, children, btnOk, btnCancel } = this.props return ( <> <div className={Style.contentHeaderDiv}> <div className={showLine && (disContent || disFooter) ? Style.contentHeader : Style.contentHeaderNoBorder} style={{ padding }} > <div className={Style.leftStyle}> { stripVisible && <div style={{ marginRight: '6px', width: '4px', height: '16px', borderRadius: '1px', backgroundColor: leftColor, display: 'inline' }}> </div>} <span>{title}</span> </div> <div className={Style.rightStyle}> {rightContent && rightContent} </div> </div> {disContent && ( <div className={showLine && disFooter ? Style.contentBody : Style.contentBodyNoBorder} style={{ padding }} > {children} </div> )} {disFooter && ( <div className={showBtn ? Style.footerBtn : null}> <div className={Style.rightStyleOk} style={btnOkStyle && btnOkStyle}> {btnOk} </div> <div className={Style.rightStyleCancel}> {btnCancel} </div> </div> )} </div> </> ) }
-
父子组件间的通讯
父子间的通讯主要有两种,第一种是父传子,第二种是子传父。
父传子:
1) 通过以value的进行传递
例如:
父组件使用value传递属性
<SyCardInfo title='小宝贝' btnOk='抚摸' btnCancel='暴扣' OkOnClick={this.OkOnClick} CancelOnClick={this.CancelOnClick} disFooter rightContent={( <div onClick={this.leftOnClick}> <span>点击</span> <Icon type='right' style={{ color: '#4A90E2' }} /> </div> )} > { array } </SyCardInfo>
子组建接收数据,使用props
let { leftColor, btnOkStyle, padding, title, rightContent, children, btnOk, btnCancel } = this.props
2)子节点的传递
意思是说在父组件调用过程中可以使用组件标签进行包裹起来传递给子组件的内标签。
例如
父组件
let array = [] for (let i = 1; i < this.state.number; i += 1) { array.push( <div key={i}> 抚摸了 {i} {' '} 次小宝贝! </div> ) } render(){ <SyCardInfo title='小宝贝' btnOk='抚摸' btnCancel='暴扣' OkOnClick={this.OkOnClick} CancelOnClick={this.CancelOnClick} disFooter rightContent={( <div onClick={this.leftOnClick}> <span>点击</span> <Icon type='right' style={{ color: '#4A90E2' }} /> </div> )} > { array } </SyCardInfo> }
子组件接收方法
<div className={showLine && disFooter ? Style.contentBody : Style.contentBodyNoBorder} style={{ padding }} > {this.props.children} </div>
- 子传父
子传父意思是说,在父组件中调用子组件中的方法或者是通过父组件去改变对应子组件中的方法以及传值问题,下面是我的一个简单的解决方案。
子组件使用父组件传递进来的一个onRef方法,通过该方法将组件间的this对象传递给父组件的回调方法,然后使用该方法做事件处理。
子组件
componentDidMount() { if (this.props.onRef) { this.props.onRef(this) } }
父组件的使用
onRef={ref => { this.onRef = ref }} // 绑定回调方法 //使用 该方法一般用作通过父组件获取新数据,使用该方法来完成数据刷新功能 OkOnClick = v => { this.onRef.onRefClike('来自父组件的关怀!') }
-
使用生命周期方法实现传入数据处理
使用生命周期方法一边用来处理复杂组件传递进来的请求信息等,可以使用生命周期方法来进行处理,然后进行渲染。
componentDidMount() { if (this.props.disFooter) { this.setStateZet('disFooter', true) } if (this.props.children) { this.setStateZet('disContent', true) } if (this.props.onRef) { this.props.onRef(this) } } setStateZet = (index, val) => { this.setState({ [index]: val }) }
-
组件方法的实现与传递
前面讲述了组件间的数据传递,其中也包括了一些方法的传递等,在此处将做一个方法的传递实现。
父组件:
//方法 OkOnClick = v => { console.log('--------这里是爸爸----------') this.setStateZet('number', this.state.number + 1) this.onRef.onRefClike('来自父组件的关怀!') } <SyCardInfo OkOnClick={this.OkOnClick} onRef={ref => { this.onRef = ref }} />
子组件:
btnOnClick = v => { if (this.props.OkOnClick) { this.props.OkOnClick(v) } } <div className={showBtn ? Style.footerBtn : null}> <div className={Style.rightStyleOk} onClick={this.btnOnClick} style={btnOkStyle && btnOkStyle}> {btnOk} </div> <div className={Style.rightStyleCancel} onClick={this.cancelOnClick}> {btnCancel} </div> </div>
组件的发布
该文档是发布至npm公有云环境
-
首先需要https://www.npmjs.com/创建npm账号
-
其次是验证邮箱https://www.npmjs.com/email-edit
-
进入组件文档根目录下
-
切换npm源路径
#查询配置 npm config get registry npm config set registry http://registry.npmjs.org
-
使用npm adduser进行账号登陆
-
上传文件目录以及配置文件
上传文件目录
{ "name": "songshao-component", # 组件名称 "version": "2.2.1", # 版本号 "description": "测试组件库", # 中文名称 "main": "dist/index.js", # 主方法js "module": "dist/index.esm.js", # 模板方法js "author": "shaosong", # npm 用户 "license": "ISC" }
-
使用npm publish进行组件上传
组件的使用
使用已发布组件库组件
npm install songshao-component -d
使用方法直接使用
import React, { Component } from 'react'
import { Icon } from 'antd'
import { SYCardInfo } from 'songshao-component'
import Style from './style.module.less'
class Index extends Component {
state={
showLine: true,
stripVisible: true,
showBtn: true,
number: 1,
newBoby: false
}
OkOnClick = v => {
console.log('--------这里是爸爸----------')
this.setStateZet('number', this.state.number + 1)
this.onRef.onRefClike('来自父组件的关怀!')
}
CancelOnClick = v => {
console.log('--------这里是爸爸----------')
this.setStateZet('number', this.state.number - 1)
}
setStateZet = (index, value) => {
this.setState({
[index]: value
})
}
leftOnClick = v => {
this.setStateZet('newBoby', true)
}
render() {
let { newBoby, stripVisible, showBtn, number } = this.state
let array = []
for (let i = 1; i < this.state.number; i += 1) {
array.push(
<div key={i}>
抚摸了
{i}
{' '}
次小宝贝!
</div>
)
}
return (
<>
<SYCardInfo
title='小宝贝'
btnOk='抚摸'
btnCancel='暴扣'
OkOnClick={this.OkOnClick}
CancelOnClick={this.CancelOnClick}
disFooter
rightContent={(
<div onClick={this.leftOnClick}>
<span>点击</span>
<Icon type='right' style={{ color: '#4A90E2' }} />
</div>
)}
onRef={ref => { this.onRef = ref }}
>
{
array
}
</SYCardInfo>
{newBoby && (
<SYCardInfo title='new小宝贝'>
点击右上角打开下面卡片
</SYCardInfo>
)}
</>
)
}
}
export default Index
图片展示
组件的规范
1.标准性
任何一个组件都应该遵守一套标准,可以使得不同区域的开发人员据此标准开发出一套标准统一的组件。
2.专一性
- 设计组件要遵循一个原则:一个组件只专注做一件事,且把这件事做好。
- 一个功能如果可以拆分成多个功能点,那就可以将每个功能点封装成一个组件,当然也不是组件的颗粒度越小越好,只要将一个组件内的功能和逻辑控制在一个可控的范围内即可。
- 页面上有一个 Table 列表和一个分页控件,就可以将 Table 封装为一个组件,分页控件 封装成一个组件,最后再把 Table组件 和 分页组件 封装成一个组件。Table 组件还可以再拆分成多个 table-column 组件,及展示逻辑等。
3.可配置性
- 一个组件,要明确它的输入和输出分别是什么。
- 组件除了要展示默认的内容,还需要做一些动态的适配,比如:一个组件内有一段文本,一个图片和一个按钮。那么字体的颜色、图片的规则、按钮的位置、按钮点击事件的处理逻辑等,都是可以做成可配置的。
- 要做可配置性,最基本的方式是通过属性向组件传递配置的值,而在组件初始化的声明周期内,通过读取属性的值做出对应的显示修改。还有一些方法,通过调用组件暴露出来的函数,向函数传递有效的值;修改全局 CSS样式;向组件传递特定事件,并在组件内监听该事件来执行函数等。
- 在做可配置性时,为了让组件更加健壮,保证组件接收到的是有效的属性、函数接收到的是有效的参数,需要做一些校验。
一. 属性的值的校验
- 属性值的类型是否是有效的。如果某个属性要求传递一个数组,那么传递过来的值不是数组时,就要抛出异常,并给出对应的提示。
- 属性是否是必填的。有的属性的值,是组件内不可缺少的时,就要是必填的,在组件初始化时要做是否传递的检查,如果没有传递,则需要抛出异常,并给出相应的提示。如果属性不是必填的,可以设定一个默认值,当属性没有被设置时,就使用默认值。
- 代码示例
// title.jsx (Title组件)
import React, { Component, PropTypes } from 'react';
import propTypes from 'prop-types';
export default class Title extends Component {
static propTypes={
title: propTypes.string,
btnOk: propTypes.string,
btnCancel: propTypes.string,
rightContent: propTypes.element || propTypes.string,
leftColor: propTypes.string,
onRef: propTypes.func,
disFooter: propTypes.bool
}
static defaultProps={
title: 'title',
btnOk: '确定',
btnCancel: '取消',
rightContent: null,
leftColor: '#4A90E2',
onRef: ref => { },
disFooter: false
}
state={
showLine: true,
stripVisible: true,
showBtn: true,
disFooter: false,
disContent: false,
}
constructor(props) {
super(props)
this.state = {
showLine: true,
stripVisible: true,
showBtn: true,
disFooter: false,
disContent: false
}
}
}
二. 函数的参数的校验
- 函数的参数校验,只要按照传统的方法进行校验即可。在函数内部顶部判断参数的值和类型,如果不满足要求,则抛出异常,并给出相应的提示。
- 代码示例
// 判断一个函数的第一个必填,且为 String 格式
// ES6 语法
changeTitle(title) {
if (typeof title !== 'string') {
throw new Error('必须传入一个 title,才能修改 title。')
}
// 满足条件,可以进行修改
this.setState({ // react 语法,修改state的值
title
})
}
rue,
showBtn: true,
disFooter: false,
disContent: false,
}
constructor(props) {
super(props)
this.state = {
showLine: true,
stripVisible: true,
showBtn: true,
disFooter: false,
disContent: false
}
}
}
二. 函数的参数的校验
- 函数的参数校验,只要按照传统的方法进行校验即可。在函数内部顶部判断参数的值和类型,如果不满足要求,则抛出异常,并给出相应的提示。
- 代码示例
// 判断一个函数的第一个必填,且为 String 格式
// ES6 语法
changeTitle(title) {
if (typeof title !== 'string') {
throw new Error('必须传入一个 title,才能修改 title。')
}
// 满足条件,可以进行修改
this.setState({ // react 语法,修改state的值
title
})
}