随着前端三大框架(Vue、React、Angular)的发展,相应的也会有element-ui、ant-design等成熟的UI组件库的出现,很大程度上提高了前端开发的效率。如果提供的组件不符合业务要求的时候,需要我们自己动手实现一个合适的组件,那么,为什么要开发组件,什么是组件化思想,实现一个组件的时候我们需要考虑哪些问题?
-
为什么要开发组件
- 组件化是对实现的分层,是更有效的代码组合方式(重复使用/一个地方使用)
- 组件化有利于代码的重组、优化、重构和维护
- 组件化有利于单元测试
-
组件化思想
简单来说,组件就是将页面中某一部分UI和对应的功能抽离出来,并封装为一个独立的整体,无论放在哪个位置,都可以使用UI和功能,复用性和灵活性较强。组件设计包括以下几个原则:
- 功能单一,短小精悍
- 避免太多参数
- 避免暴露组件内部实现
- 避免直接操作DOM
- 入口处检查参数的有效性,出口处检查返回的正确性
- 引用透明,无副作用
- 充分分隔变化的部分
-
怎么实现一个优秀的组件
实现组件之前我们需要考虑这样几个问题
- 组件是否还可以拆分,是否还需要拆分,合理划分颗粒度
- 组件的依赖是够可以缩减
- 组件是否对其他组件造成侵入(组件封装性不够或自身越界操作)
- 组件是否可复用于其他类似场景
- 其他人使用该组件时,是否满足简单上手,接口设计符合大众习惯的条件
- 业务不需要这个功能时,是否方便清除
接下来我们以实现一个分页组件为例,来讨论实现一个组件的详细过程
分页组件需具备以下功能:合理的动态显示页码;点击某一个页码,响应的会去请求对应数据并展示至页面上;点击前一页,后一页可实现相应的翻页操作;当前页码为第一页,则前一页无法点击,当前页码为最后一页,则无法点击下一页。
现在我们开始考虑这样几个问题,组件入口(属性)和组件出口(事件)、可复用、UI和功能
要实现上图这样的一个分页组件,其实可以进一步拆分,拆分为两个部分,上面的列表展示和下面的分页页码,这样可以满足复用性。
分页页码的实现,需要当前页码(currentPage)、开始页码(startPage)、分组页码(groupCount)、总页数(totalPage)等属性(组件入口);需要页码跳转(pageCallbackFn)事件(组件出口)。
展示列表的实现,需要列表数据(dataList),当点击分页页码执行页码变化操作时,展示列表需要有响应的变化,所以,我们可以将页码跳转方法定义在分页页码的父组件中,这样,我们的组件结构则基本形成,Pagecontainer (分页组件整体)的子组件为Pagecomponent(分页页码)。
分页展示核心思想:当总页数不大于10时,显示全部页码;当总页数大于10时,始终显示首尾页码,当当前页码大于分组页码时,显示省略号。
-
初始化项目
//安装构建工具,如已安装,可跳过该步骤 npm install -g create-react-app //初始化项目 create-react-app study_code
初始化项目后,可删除无用代码,添加新的文件和目录,我的项目目录如下图所示
-
本地json模拟数据
项目目录下新建
tsconfig.json
文件,并添加一下代码[ {"id":1,"name":"hello1"}, {"id":2,"name":"hello2"}, {"id":3,"name":"hello3"}, {"id":4,"name":"hello4"}, {"id":5,"name":"hello5"}, {"id":6,"name":"hello6"}, {"id":7,"name":"hello7"}, {"id":8,"name":"hello8"}, {"id":9,"name":"hello9"}, {"id":10,"name":"hello10"}, {"id":11,"name":"hello11"}, {"id":12,"name":"hello12"}, {"id":13,"name":"hello13"}, {"id":14,"name":"hello14"} ]
-
Pagecontainer
新建
Pagecontainer.js
文件,并添加以下代码import React, {Component} from 'react' import Pagecomponent from './pageComponent.js' import data from '../mock/tsconfig.json' class Pagecontainer extends Component { constructor() { super(); this.state = { dataList:[], pageConfig: { totalPage: data.length //总条数 } } this.getCurrentPage = this.getCurrentPage.bind(this) } getCurrentPage(currentPage) { this.setState({ dataList:data[currentPage-1].name }) } render() { return ( <div> <div style={{padding:'0 300px'}}> {this.state.dataList} </div> <Pagecomponent pageConfig={this.state.pageConfig} pageCallbackFn={this.getCurrentPage}/> </div> ) } } export default Pagecontainer
-
Pagecomponent
新建
Pagecomponent.js
文件,并添加以下代码import React, {Component} from 'react' import './pageComponent.css' class Pagecomponent extends Component { constructor(props) { super(props) this.state = { currentPage: 1, //当前页码 groupCount: 5, //页码分组,显示7个页码,其余用省略号显示 startPage: 1, //分组开始页码 totalPage:1 //总页数 } this.createPage = this.createPage.bind(this) } componentDidMount() { this.setState({ totalPage: this.props.pageConfig.totalPage }) this.props.pageCallbackFn(this.state.currentPage) } createPage() { //const {totalPage} = this.props.pageConfig; const {currentPage, groupCount, startPage,totalPage} = this.state; let pages = [] //上一页 pages.push(<li className={currentPage === 1 ? "nomore" : null} onClick={this.prePageHandeler.bind(this)} key={0}> 上一页</li>) if (totalPage <= 10) { /*总页码小于等于10时,全部显示出来*/ for (let i = 1; i <= totalPage; i++) { pages.push(<li key={i} onClick={this.pageClick.bind(this, i)} className={currentPage === i ? "activePage" : null}>{i}</li>) } } else { /*总页码大于10时,部分显示*/ //第一页 pages.push(<li className={currentPage === 1 ? "activePage" : null} key={1} onClick={this.pageClick.bind(this, 1)}>1</li>) let pageLength = 0; if (groupCount + startPage > totalPage) { pageLength = totalPage } else { pageLength = groupCount + startPage; } //前面省略号(当当前页码比分组的页码大时显示省略号) if (currentPage >= groupCount) { pages.push(<li className="" key={-1}>···</li>) } //非第一页和最后一页显示 for (let i = startPage; i < pageLength; i++) { if (i <= totalPage - 1 && i > 1) { pages.push(<li className={currentPage === i ? "activePage" : null} key={i} onClick={this.pageClick.bind(this, i)}>{i}</li>) } } //后面省略号 if (totalPage - startPage >= groupCount + 1) { pages.push(<li className="" key={-2}>···</li>) } //最后一页 pages.push(<li className={currentPage === totalPage ? "activePage" : null} key={totalPage} onClick={this.pageClick.bind(this, totalPage)}>{totalPage}</li>) } //下一页 pages.push(<li className={currentPage === totalPage ? "nomore" : null} onClick={this.nextPageHandeler.bind(this)} key={totalPage + 1}>下一页</li>) return pages; } //页码点击 pageClick(currentPage) { const {groupCount} = this.state const getCurrentPage = this.props.pageCallbackFn; //当 当前页码 大于 分组的页码 时,使 当前页 前面 显示 两个页码 if (currentPage >= groupCount) { this.setState({ startPage: currentPage - 2, }) } if (currentPage < groupCount) { this.setState({ startPage: 1, }) } //第一页时重新设置分组的起始页 if (currentPage === 1) { this.setState({ startPage: 1, }) } this.setState({ currentPage }) //将当前页码返回父组件 getCurrentPage(currentPage) } //上一页事件 prePageHandeler() { let {currentPage} = this.state if (--currentPage === 0) { return false; } this.pageClick(currentPage) } //下一页事件 nextPageHandeler() { let {currentPage,totalPage} = this.state if (++currentPage > totalPage) { return false; } this.pageClick(currentPage); } //根据输入框有效值跳转相应页码 jumpTo(e){ let val = parseInt(e.target.value); if(!val){ return false; } this.pageClick(val); e.target.value = ''; } render() { const pageList = this.createPage(); return ( <ul className="page-container"> {pageList} <span>总共{this.props.pageConfig.totalPage}条,共{this.state.totalPage}页,到第</span> <input className="jumpTo" type="text" onBlur={this.jumpTo.bind(this)}/> <span>页</span> </ul> ) } } export default Pagecomponent
新建
Pagecomponent.css
文件,并添加以下代码.page-container:after { content: '.'; display: block; height: 0; overflow: hidden; clear: both; } .page-container { zoom: 1; line-height:30px; } .page-container li { list-style: none; float: left; padding: 0px 8px; cursor: pointer; border: 1px solid #ccc; height: 28px; line-height: 28px; margin-right: 8px; } .page-container li:last-child { margin-right: 0px; } /*当前页样式*/ .activePage { color: #fff !important; background: #2292bd; border-color: #2292bd !important; } /*没有上一页和下一页时样式*/ .nomore { color: #b5b5b5 !important; cursor: not-allowed !important; } .jumpTo{ width:30px; }
-
APP.js中引入Pagecontainer 组件
import React, { Component } from 'react'; import './App.css'; import PageContainer from './pagination/pageContainer.js' class App extends Component { render() { return <PageContainer></PageContainer> } } export default App;
执行npm start即可运行项目,查看实际效果