课堂目标
- 掌握第三⽅组件正确使⽤⽅式
- 能设计并实现⾃⼰的组件
- 了解常⻅组件优化技术
知识要点
- 使⽤antd
- 设计并实现表单控件
- 实现弹窗类组件
- 实现树组件
- 使⽤PureComponent、memo
资源
快速开始
npx create-react-app my-app
cd my-app
npm start
使⽤第三⽅组件
不必eject,直接安装: npm install antd --save
范例:试⽤ ant-design组件库
import React, { Component } from 'react'
import Button from 'antd/lib/button'
import 'antd/dist/antd.css'
class App extends Component {
render() {
return (
<div className="App">
<Button type="primary">Button</Button>
</div>
)
}
}
export default App
配置按需加载
安装react-app-rewired取代react-scripts,可以扩展webpack的配置 ,类似vue.confifig.js
npm install react-app-rewired customize-cra babel-plugin-import -D
根⽬录创建config-overrides.js
const { override, fixBabelImports } = require('customize-cra')
module.exports = override(
fixBabelImports('import', {
//antd按需加载
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
})
)
//修改package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
⽀持装饰器配置
npm install -D @babel/plugin-proposal-decorators
const { addDecoratorsLegacy } = require('customize-cra')
module.exports = override(
//...,
addDecoratorsLegacy() //配置装饰器
)
HocPage.js【按需加载和实现装饰器之后的⻚⾯如下】
import React, { Component } from 'react'
import { Button } from 'antd'
const foo = (Cmp) => (props) => {
return (
<div className="border">
<Cmp {...props} />
</div>
)
}
const foo2 = (Cmp) => (props) => {
return (
<div className="border" style={{ border: 'solid 1px red' }}>
<Cmp {...props} />
</div>
)
}
@foo
@foo2
class Child extends Component {
render() {
return <div className="border">child</div>
}
}
/* function Child(props) {
return <div className="border">child</div>;
} */
@foo2
class HocPage extends Component {
render() {
// const Foo = foo2(foo(Child));
return (
<div>
<h1>HocPage</h1>
<Child />
<Button type="dashed">click</Button>
</div>
)
}
}
export default HocPage
表单组件设计思路
表单组件设计思路:
- 表单组件要求实现数据收集、校验、提交等特性,可通过⾼阶组件扩展
- ⾼阶组件给表单组件传递⼀个input组件包装函数接管其输⼊事件并统⼀管理表单数据
- ⾼阶组件给表单组件传递⼀个校验函数使其具备数据校验功能
antd表单试⽤
import React, { Component } from 'react'
import { Form, Input, Icon, Button } from 'antd'
const FormItem = Form.Item
//校验规则
const nameRules = { required: true, message: 'please input your name' }
const passwordRules = { required: true, message: 'please input your password' }
@Form.create()
class FormPageDecorators extends Component {
handleSubmit = () => {
/* const { getFieldsValue, getFieldValue } = this.props.form;
console.log("submit", getFieldsValue()); */
const { validateFields } = this.props.form
validateFields((err, values) => {
if (err) {
console.log('err', err)
} else {
console.log('submit', values)
}
})
}
render() {
const { getFieldDecorator } = this.props.form
// console.log(this.props.form);
return (
<div>
<h1>FormPageDecorators</h1>
<Form>
<FormItem label="姓名">
{getFieldDecorator('name', { rules: [nameRules] })(
<Input prefix={<Icon type="user" />} />
)}
</FormItem>
<FormItem label="密码">
{getFieldDecorator('password', {
rules: [passwordRules],
})(
<Input
type="password"
prefix={<Icon type="lock" />}
/>
)}
</FormItem>
<FormItem label="姓名">
<Button type="primary" onClick={this.handleSubmit}>
提交
</Button>
</FormItem>
</Form>
</div>
)
}
}
export default FormPageDecorators
// export default Form.create()(FormPageDecorators);
表单组件-具体实现
表单基本结构,创建MyFormPage.js
import React, { Component } from 'react'
import kFormCreate from '../../components/kFormCreate'
const nameRules = { required: true, message: 'please input your name!' }
const passwordRules = {
required: true,
message: 'please input your password!',
}
class MyFormPage extends Component {
handleSubmit = () => {
const { getFieldValue } = this.props
const res = {
name: getFieldValue('name'),
password: getFieldValue('password'),
}
console.log('hah', res)
}
handleSubmit2 = () => {
// 加⼊校验
const { validateFields } = this.props
validateFields((err, values) => {
if (err) {
console.log('validateFields', err)
} else {
console.log('submit', values)
}
})
}
render() {
const { getFieldDecorator } = this.props
return (
<div>
<h1>MyFormPage</h1>
<div>
{getFieldDecorator('name', { rules: [nameRules] })(
<input type="text" />
)}
{getFieldDecorator('password', [nameRules])(
<input type="password" />
)}
</div>
<button onClick={this.handleSubmit2}>submit</button>
</div>
)
}
}
export default kFormCreate(MyFormPage)
⾼阶组件kFormCreate:扩展现有表单,./components/KFormTest.js
import React, { Component } from 'react'
export default function kFormCreate(Cmp) {
return class extends Component {
constructor(props) {
super(props)
this.options = {} //各字段选项
this.state = {} //各字段值
}
handleChange = (e) => {
let { name, value } = e.target
this.setState({ [name]: value })
}
getFieldValue = (field) => {
return this.state[field]
}
validateFields = (callback) => {
const res = { ...this.state }
const err = []
for (let i in this.options) {
if (res[i] === undefined) {
err.push({ [i]: 'error' })
}
}
if (err.length > 0) {
callback(err, res)
} else {
callback(undefined, res)
}
}
getFieldDecorator = (field, option) => {
this.options[field] = option
return (InputCmp) => (
<div>
{
// 由React.createElement⽣成的元素不能修改,需要克隆⼀份再扩展
React.cloneElement(InputCmp, {
name: field,
value: this.state[field] || '', //控件值
onChange: this.handleChange, //控件change事件处理
})
}
</div>
)
}
render() {
return (
<div className="border">
<Cmp
{...this.props}
getFieldDecorator={this.getFieldDecorator}
getFieldValue={this.getFieldValue}
validateFields={this.validateFields}
/>
</div>
)
}
}
}
antd表单试⽤
弹窗类组件-设计思路
弹窗类组件的要求弹窗内容在A处声明,却在B处展示。react中相当于弹窗内容看起来被render到⼀个组件⾥⾯去,实际改变的是⽹⻚上另⼀处的DOM结构,这个显然不符合正常逻辑。但是通过使⽤框架提供的特定API创建组件实例并指定挂载⽬标仍可完成任务。
// 常⻅⽤法如下:Dialog在当前组件声明,但是却在body中另⼀个div中显示
;<div class="foo">
<div> ... </div>
{needDialog && (
<Dialog>
<header>Any Header</header>
<section>Any content</section>
</Dialog>
)}
</div>
弹窗类组件-具体实现-⽅案1:Portal
传送⻔,react v16之后出现的portal可以实现内容传送功能。
范例:Dialog组件
// Diallog.js
import React, { Component } from 'react'
import { createPortal } from 'react-dom'
import './index.scss'
export default class Diallog extends Component {
constructor(props) {
super(props)
const doc = window.document
this.node = doc.createElement('div')
doc.body.appendChild(this.node)
}
componentWillUnmount() {
window.document.body.removeChild(this.node)
}
render() {
const { hideDialog } = this.props
return createPortal(
<div className="dialog">
{this.props.children}
{typeof hideDialog === 'function' && (
<button onClick={hideDialog}>关掉弹窗</button>
)}
</div>,
this.node
)
}
}
Diallog/index.scss
.dialog {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
line-height: 30px;
width: 400px;
height: 300px;
transform: translate(50%, 50%);
border: solid 1px gray;
text-align: center;
}
弹窗类组件-具体实现-⽅案2:unstable_renderSubtreeIntoContainer
在v16之前,实现“传送⻔”,要⽤到react中两个秘⽽不宣的React API
export class Dialog2 extends React.Component {
render() {
return null
}
componentDidMount() {
const doc = window.document
this.node = doc.createElement('div')
doc.body.appendChild(this.node)
this.createPortal(this.props)
}
componentDidUpdate() {
this.createPortal(this.props)
}
componentWillUnmount() {
unmountComponentAtNode(this.node)
window.document.body.removeChild(this.node)
}
createPortal(props) {
unstable_renderSubtreeIntoContainer(
this, //当前组件
<div className="dialog">{props.children}</div>, // 塞进传送⻔的JSX
this.node // 传送⻔另⼀端的DOM node
)
}
}
总结⼀下:
- Dialog什么都不给⾃⼰画,render返回⼀个null就够了;
- 它做得事情是通过调⽤createPortal把要画的东⻄画在DOM树上另⼀个⻆落。
树形组件-设计思路
递归:⾃⼰调⽤⾃⼰
如计算f(n)=f(n-1)*n; n>0, f(1)=1
function foo(n) {
return n === 1 ? 1 : n * foo(n - 1)
}
react中实现递归组件更加纯粹,就是组件递归渲染即可。假设我们的节点组件是TreeNode,它的render中只要发现当前节点拥有⼦节点就要继续渲染⾃⼰。节点的打开状态可以通过给组件⼀个open状态来维护。
树形组件-具体实现
TreeNode.js
import React, { Component } from 'react'
import TreeNode from '../../components/TreeNode'
//数据源
const treeData = {
key: 0, //标识唯⼀性
title: '全国', //节点名称显示
children: [
//⼦节点数组
{
key: 6,
title: '北⽅区域',
children: [
{
key: 1,
title: '⿊⻰江省',
children: [
{
key: 6,
title: '哈尔滨',
},
],
},
{
key: 2,
title: '北京',
},
],
},
{
key: 3,
title: '南⽅区域',
children: [
{
key: 4,
title: '上海',
},
{
key: 5,
title: '深圳',
},
],
},
],
}
export default class TreePage extends Component {
render() {
return (
<div>
<h1>TreePage</h1>
<TreeNode data={treeData} />
</div>
)
}
}
TreeNode.js
import React, { Component } from 'react'
import classnames from 'classnames' //先安装下npm install classnames
export default class TreeNode extends Component {
constructor(props) {
super(props)
this.state = {
expanded: false,
}
}
handleExpanded = () => {
this.setState({
expanded: !this.state.expanded,
})
}
render() {
const { title, children } = this.props.data
const { expanded } = this.state
const hasChildren = children && children.length > 0
return (
<div>
<div className="nodeInner" onClick={this.handleExpanded}>
{hasChildren && (
<i
className={classnames(
'tri',
expanded ? 'tri-open' : 'tri-close'
)}
></i>
)}
<span>{title}</span>
常⻅组件优化技术 定制组件的shouldComponentUpdate钩⼦
范例:通过shouldComponentUpdate优化组件
</div>
{expanded && hasChildren && (
<div className="children">
{children.map((item) => {
return <TreeNode key={item.key} data={item} />
})}
</div>
)}
</div>
)
}
}
树组件css
.nodeInner {
cursor: pointer;
}
.children {
margin-left: 20px;
}
.tri {
width: 20px;
height: 20px;
margin-right: 2px;
padding-right: 4px;
}
.tri-close:after,
.tri-open:after {
content: "";
display: inline-block;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-left: 8px solid black;
border-bottom: 6px solid transparent;
}
.tri-open:after {
transform: rotate(90deg);
}
常⻅组件优化技术-定制组件的shouldComponentUpdate钩⼦
范例:通过shouldComponentUpdate优化组件
import React, { Component } from 'react'
export default class CommentList extends Component {
constructor(props) {
super(props)
this.state = { comments: [] }
}
componentDidMount() {
setInterval(() => {
this.setState({
comments: [
{
author: '⼩明',
body: '这是⼩明写的⽂章',
},
{
author: '⼩红',
body: '这是⼩红写的⽂章',
},
],
})
}, 1000)
}
render() {
const { comments } = this.state
return (
<div>
<h1>CommentList</h1>
{comments.map((c, i) => {
return <Comment key={i} data={c} />
})}
</div>
)
}
}
class Comment extends Component {
shouldComponentUpdate(nextProps, nextState) {
const { author, body } = nextProps.data
const { author: nowAuthor, body: nowBody } = this.props.data
if (body === nowBody && author === nowAuthor) {
return false //如果不执⾏这⾥,将会多次render
}
return true
}
render() {
console.log('hah')
const { body, author } = this.props.data
return (
<div>
<p>作者: {author}</p>
<p>正⽂:{body}</p>
<p>---------------------------------</p>
</div>
)
}
}
常⻅组件优化技术-PureComponent
定制了shouldComponentUpdate后的Component
import React, { Component, PureComponent } from 'react'
export default class PuerComponentPage extends PureComponent {
constructor(props) {
super(props)
this.state = {
counter: 0,
obj: {
num: 100,
},
}
}
setCounter = () => {
this.setState({
counter: 1,
obj: {
num: 200,
},
})
console.log('setCounter')
}
render() {
console.log('render')
const { counter, obj } = this.state
return (
<div>
<button onClick={this.setCounter}>setCounter</button>
<div>counter: {counter}</div>
<div>obj.num: {obj.num}</div>
</div>
)
}
}
缺点是必须要⽤class形式,⽽且要注意是浅⽐较
常⻅组件优化技术-React.memo
React.memo(...) 是React v16.6引进来的新属性。它的作⽤和 React.PureComponent 类似,是⽤来控制函数组件的重新渲染的。 React.memo(...) 其实就是函数组件的 React.PureComponent 。
import React, { Component, PureComponent, memo } from 'react'
export default class MemoPage extends Component {
constructor(props) {
super(props)
this.state = {
counter: 0,
obj: { num: -1 },
}
}
setCounter = () => {
this.setState({
counter: 1 /* ,
obj: {
num: 100,
}, */,
})
}
render() {
const { counter } = this.state
return (
<div>
<h1>MemoPage</h1>
<button onClick={this.setCounter}>按钮</button>
{/* <PuerCounter counter={counter} obj={obj} /> */}
<PuerCounter counter={counter} />
</div>
)
}
}
const PuerCounter = memo((props) => {
console.log('render')
return <div>{props.counter}</div>
})
作业
实现下图:提示:可以使⽤antd的 Card、Input、Tree、Button、Form、Table
React组件化2
- 课堂⽬标
- 知识要点
- 资源
- 知识点
- 快速开始
- 使⽤第三⽅组件
- 配置按需加载
- 表单组件设计与实现
- antd表单试⽤
- 表单组件设计思路
- 表单组件实现
- 弹窗类组件设计与实现
- 设计思路
- 具体实现
- ⽅案1:Portal
- ⽅案2:unstable_renderSubtreeIntoContainer
- 树形组件设计与实现
- 设计思路
- 实现
- 常⻅组件优化技术
- 定制组件的shouldComponentUpdate钩⼦
- PureComponent
- React.memo
- 作业