React组件化
1 Ant-Design第三方库介绍和按需加载方式
Ant-Design是React非常流行的第三方库,企业级产品设计体系,创造高效愉悦的工作体验。
1.1 安装与初始化
1.1.1 安装
1.1.1.1 使用 npm
我们推荐使用 npm 或 yarn 的方式进行开发,不仅可在开发环境轻松调试,也可放心地在生产环境打包部署使用,享受整个生态圈和工具链带来的诸多好处。
npm install antd --save
当然了这里还是推荐国内用户使用淘宝的npm镜像使用cnpm命令安装或者是翻墙
1.1.1.2 使用yarn安装
yarn add antd
1.2初始化
既然我们已经安装成功就看看如何把对应的组件展示出来,例如我需要一个按钮。
这里我新生成了个项目,没有删除目录内容。
1.2.1 全局导入
首先在app.js中导入对应的组件以及样式库
import Button from 'antd/es/button/' //引入ant的按钮组件
import 'antd/dist/antd.css' //引入ant的样式库
然后在在任意一处位置使用ant的Button组件,详细内容看ant的文档
<Button type="primary">登录</Button>
虽然没用如何问题但是我们需要注意,在以后开发React项目的过程中这种导入方式是很消耗性能的,而且使用起来也不方便,因此ant给我们提供了一种解决方案即高级配置。
1.2.2 高级配置 按需加载
上面例子在实际开发中还有一些优化的空间,比如无法进行主题配置,而且上面的例子加载了全部的 antd 组件的样式(gzipped 后一共大约 60kb)。
此时我们需要对 create-react-app 的默认配置进行自定义,这里我们使用 react-app-rewired (一个对 create-react-app 进行自定义配置的社区解决方案)。
引入 react-app-rewired 并修改 package.json 里的启动配置。由于新的 react-app-rewired@2.x 版本的关系,你还需要安装 customize-cra。
$ yarn add react-app-rewired customize-cra
# 使用less-loader@6.0.0
$ yarn add react-app-rewired customize-cra@next
/* package.json */
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
}
然后在项目根目录创建一个 config-overrides.js
用于修改默认配置。
const { override, fixBabelImports } = require('customize-cra');
module.exports = override(
fixBabelImports('import',{
libraryName:'antd',
libraryDirectory:'es',
style:'css'
})
)
使用 babel-plugin-import
注意:antd 默认支持基于 ES module 的 tree shaking,js 代码部分不使用这个插件也会有按需加载的效果。
babel-plugin-import 是一个用于按需加载组件代码和样式的 babel 插件(原理),现在我们尝试安装它并修改 config-overrides.js
文件。
$ yarn add babel-plugin-import
经过了上面的一些配置我们就可以在组件中使用按需导入的方式
import { Button } from 'antd'
因为css在 config-overrides.js
我们已经导出去了所以这里不需要再次导入,然后随便找一个位置就可以使用ant组件了!
真的是一劳永逸
2 聪明式组件&傻瓜式组件
基本原则:聪明组件(容器组件)负责数据获取,傻瓜组件(展示组件)负责根据根据props显示信息内容
优势:
- 逻辑和内容展示分离
- 重用性高
- 复用性高
- 易于测试
这里就将使用一个评论列表组件演示下什么是聪明组件什么是傻瓜组件
import React, { Component } from "react";
function Comment(props) {
const { id, content, author } = props.comment;
return (
<div>
<p>{id}</p>
<p>{content}</p>
<p>{author}</p>
</div>
);
}
class CommentList extends Component {
constructor(props) {
super(props);
this.state = {
comments: []
};
}
render() {
setTimeout(() => {
this.setState({
comments: [
{
id: 1,
author: "FaceBook",
content: "React牛逼"
},
{
id: 2,
author: "尤雨溪",
content: "Vue更牛逼"
}
]
});
}, 1000);
return (
<div>
{this.state.comments.map((item, i) => {
return <Comment key={i} comment={item} />;
})}
</div>
);
}
}
export default CommentList;
2.1 聪明组件
CommentList就是一个聪明组件,我们可以看到它负责了数据获取并且给Comment提供了容器。
class CommentList extends Component {
constructor(props) {
super(props);
this.state = {
comments: []
};
}
render() {
//模拟异步ajax请求数据
setTimeout(() => {
this.setState({
comments: [
{
id: 1,
author: "FaceBook",
content: "React牛逼"
},
{
id: 2,
author: "尤雨溪",
content: "Vue更牛逼"
}
]
});
}, 1000);
return (
//组件传值
<div>
{this.state.comments.map((item, i) => {
return <Comment key={i} comment={item} />;
})}
</div>
);
}
}
2.2 傻瓜组件
Comment在这里的角色就是一个傻瓜组件,他仅仅负责接收从CommentList来的数据然后渲染出对应的内容。
function Comment(props) {
const { id, content, author } = props.comment;
return (
<div>
<p>{id}</p>
<p>{content}</p>
<p>{author}</p>
</div>
);
}
所以在抽离组件的过程中需要思考这个组件将来是一个什么组件如果是一个傻瓜式组件我就让他单纯的展示,如果是一个聪明组件我们就需要使用class的方式声明,然后进行一些操作。
3 组件渲染优化解决方案
呐,上一节我们使用聪明式组件&傻瓜式组件实现了一个评论列表,现在的需求是我希望他可以实时更新,每一秒向服务器发起一次请求进行轮询更新,那这里就有人有想法了,不就是把setTimeout
换成setInterval
不就好了。
setInterval(() => {
this.setState({
comments: [
{
id: 1,
author: "FaceBook",
content: "React牛逼"
},
{
id: 2,
author: "尤雨溪",
content: "Vue更牛逼"
}
]
});
}, 1000);
ennn,事实上这样是没错的,也可以进行轮询,但是有没有思考过,我1秒进行一次轮询,一天就是3600X24次的轮询,对服务器的压力以及对本地计算机的压力来说都很大(反正我的电脑现在已经开始嗡嗡的叫唤了)
对于React来说外界的数据不管有没有变化React都会进行更新,但是这个是我们不希望的,我们希望的是数据没用变化React就不进行render
3.1 shouldComponentUpdate 生命周期
这里我们可以使用生命周期里的一个钩子函数shouldComponentUpdate
所以在这里改造一下我们的Comment组件
class Comment extends Component {
//性能优化点
shouldComponentUpdate(nextProps, nextState, nextContext) {
if (nextProps.comment.content === this.props.comment.content) {
return false;
} else {
return true;
}
}
render() {
const { id, content, author } = this.props.comment;
console.log("render");
return (
<div>
<p>{id}</p>
<p>{content}</p>
<p>{author}</p>
</div>
);
}
}
使用shouldComponentUpdate
去判断一下数据有没有发生变化 如果没有发生变化就阻止重新渲染。
3.2 继承于PureComponent
//解构出PureComponent组件
import React, { Component, PureComponent } from "react";
随后我们的Comment
组件改造成继承于PureComponent
class Comment extends PureComponent {
render() {
const { id, content, author } = this.props.comment;
return (
<div>
<p>{id}</p>
<p>{content}</p>
<p>{author}</p>
</div>
);
}
}
为什么要继承于PureComponent
组件?是因为PureComponent
内部重写了shouldComponentUpdate
,当然这里好奇PureComponent
是如何重写的,如何进行比较的这里就不展开聊了,有兴趣的小伙伴可以在node_modules
目录中找到PureComponent
组件看看。
但是注意!代码改造到这一步按理来说应该是没有问题了,但是调试过程中我们可以发现它还是在继续渲染,这是因为PureComponent
是浅比较实现的函数类。
那我们的comments
一个数组里嵌套对象这种显然使用浅比较无法得出结果。
那既然这样,我们是不是在父组件中在传值的过程中不传递一个复杂的数据结构了,我们直接传递对应的值是不是就可以解决了?
import React, { Component, PureComponent } from "react";
class Comment extends PureComponent {
render() {
console.log("render");
// 2.修改字段,解构
const { id, content, author } = this.props;
return (
<div>
<p>{id}</p>
<p>{content}</p>
<p>{author}</p>
</div>
);
}
}
class CommentList extends Component {
constructor(props) {
super(props);
this.state = {
comments: []
};
}
render() {
setInterval(() => {
this.setState({
comments: [
{
id: 1,
author: "FaceBook",
content: "React牛逼"
},
{
id: 2,
author: "尤雨溪",
content: "Vue更牛逼"
}
]
});
}, 1000);
return (
<div>
{this.state.comments.map((item, i) => {
return (
// 1.改造成传递简单的值
<Comment
key={i}
id={item.id}
content={item.content}
author={item.author}
/>
);
})}
</div>
);
}
}
export default CommentList;
经过调试后发现确实是解决了,而且效果还不错。
当然我们还可以使用ES6的剩余运算符去优化我们的代码,提升境界。
<Comment key={i} {...item} />
3.2 React.meom
注意这是一种高阶组件用法,具体的内容会在后面解释。
const Comment = React.memo(({ id, content, author }) => {
return (
<div>
<p>{id}</p>
<p>{content}</p>
<p>{author}</p>
</div>
);
});
用法与PureComponent
类似但是更适合函数式声明的组件,当然你要是类声明的组件也可以用。
4 组件组合而非继承实现代码重用
React有十分强大的组合模式,我们推荐使用组合而非继承实现组件间的代码重用。
这里设计一个Demo,有这样一个需求,我需要一个信息框的组件,这个信息框可重用的组件。
首先我新建了一个组件Compond
import React, { Component } from "react";
function Dialog(props) {
//这里的children就跟Vue中的匿名插槽类似
return <div>{props.children}</div>;
}
function WelcomeDialog() {
return (
<div>
<Dialog>
<h2>Hello</h2>
<p>F_picacho</p>
</Dialog>
</div>
);
}
class Compond extends Component {
render() {
return (
<div>
<WelcomeDialog />
</div>
);
}
}
export default Compond;
这个过程就是我们将Dialog
组件组合到了WelcomeDialog
中,比如你还有他的信息框,例如提示,警告等等。
你看也可以传递一些其他的内容 例如样式属性,这里顺便写一下行内的写法。
import React, { Component } from "react";
function Dialog(props) {
//这里的children就跟Vue中的匿名插槽类似
return (
<div style={{ border: `3px solid ${props.color}` }}>{props.children}</div>
);
}
function WelcomeDialog() {
return (
<div>
<Dialog color="pink">
<h2>Hello</h2>
<p>F_picacho</p>
</Dialog>
</div>
);
}
class Compond extends Component {
render() {
return (
<div>
<WelcomeDialog />
</div>
);
}
}
export default Compond;
甚至还能传递一个组件,这里就直接使用Ant的按钮作为例子
import React, { Component } from "react";
import { Button } from "antd";
function Dialog(props) {
return (
<div style={{ border: `3px solid ${props.color}` }}>
{props.children}
<div>{props.btn}</div>
</div>
);
}
function WelcomeDialog() {
const infoBtn = <Button type="info">确认</Button>;
return (
<div>
<Dialog color="pink" btn={infoBtn}>
<h2>Hello</h2>
<p>F_picacho</p>
</Dialog>
</div>
);
}
class Compond extends Component {
render() {
return (
<div>
<WelcomeDialog />
</div>
);
}
}
export default Compond;
这个感觉就又有点像Vue的具名插槽了,但是在React中没有插槽,这种方式叫做组合。
5 HOC高阶组件 !重要
组件设计的目的:保证功能的单一性。
高阶组件不是组件,本质上是一个函数,高阶组件接收一个组件或者多个组件,返回一个新的组件,则当前组件为高阶组件,好比就是你给我一个赛亚人到我的高阶组件里,我给你返回一个超级赛亚人。高阶组件对当前组件进行一系列的加工。
我们之前使用过一次React提供的高阶组件React.meom
,这里就新建了一个Hoc.Js设计了一个Demo演示下如何自己写出一个高阶组件。
import React, { Component } from "react";
// 本质上是一个函数,高阶组件接收一个组件或者多个组件,返回一个新的组件.
const highOrderCom = Comp => {
// 返回一个新组件
const NewComponent = props => {
// 属性代理
const attr = { type: "高阶组件" };
return <Comp {...props} {...attr}></Comp>;
};
return NewComponent;
};
class Hoc extends Component {
render() {
return (
<div>
<h3>{this.props.type}</h3>
</div>
);
}
}
// 抛出高阶组件加工后的新组件
export default highOrderCom(Hoc);
这是一种普通的高阶组件,把普通组件传递进去,添加了一些属性,我们还可以实现重写高阶组件内部的生命周期。
import React, { Component } from "react";
function highOrderCom(Comp) {
return class extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log("ajax请求");
}
render() {
return <Comp {...this.props} name="react"></Comp>;
}
};
}
class Hoc extends Component {
render() {
return (
<div>
<h3>{this.props.name}</h3>
</div>
);
}
}
// 抛出高阶组件加工后的新组件
export default highOrderCom(Hoc);
我们以后可以使用高阶组件实现更加强大的动态功能
5.1 高阶组件的应用
5.1.1 打印日志的高阶组件实现和链式调用
import React, { Component } from "react";
// 打印日志高阶组件
const withLog = Comp => {
console.log(Comp.name + "渲染了");
const newCom props => {
return <Comp {...props} />;
}
return newCom;
};
function highOrderCom(Comp) {
return class extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log("ajax请求");
}
render() {
return <Comp {...this.props} name="react"></Comp>;
}
};
}
class Hoc extends Component {
render() {
return (
<div>
<h3>{this.props.name}</h3>
</div>
);
}
}
// 抛出高阶组件加工后的新组件 从内到外外 链式调用
export default highOrderCom(withLog(withLog(Hoc)));
最终结果应该是三条分别是'Hoc渲染了','newCom'渲染了以及'ajax请求'发生的顺序也跟代码里最后一样 从内到外链式调用,但是这么写多多少少有那么一丝丝奇怪,像套娃一样,所以这里再推一个ES7的写法,装饰器。
首先我们要安装一个babel插件进行语法转换
npm install --save-dev babel-plugin-transform-decorators-legacy @babel/plugin-proposal-decorators
下载完成后在config-overrides.js
修改对应配置
const {
override,
fixBabelImports, //按需加载配置函数
addBabelPlugins //babel插件配置函数
} = require("customize-cra");
module.exports = override(
fixBabelImports("import", {
libraryName: "antd",
libraryDirectory: "es",
style: "css"
}),
addBabelPlugins(["@babel/plugin-proposal-decorators", { legacy: true }]) //支持装饰器
然后react就可以支持更加高级的语法了
在使用装饰器之前我们应该明确我们对谁装饰就在谁之前写装饰器的语法,例如我们要装饰的是Hoc所以就在Hoc之前写
import React, { Component } from "react";
// 打印日志高阶组件
const withLog = Comp => {
console.log(Comp.name + "渲染了");
const newCow = props => {
return <Comp {...props} />;
};
return newCow;
};
function highOrderCom(Comp) {
return class extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log("ajax请求");
}
render() {
return <Comp {...this.props} name="react"></Comp>;
}
};
}
//注意顺序 由下到上
@highOrderCom
@withLog
@withLog
class Hoc extends Component {
render() {
return (
<div>
<h3>{this.props.name}</h3>
</div>
);
}
}
// 抛出高阶组件加工后的新组件 从内到外外 链式调用
export default Hoc;
5.1.2 页面复用
比如我现在有一个电影列表它有两个分类A,B,A和B都是同样的样式页面里面的数据不同。
MovieA.js
MovieB.js
import React, { Component } from "react";
class MovieA extends Component {
constructor(porps) {
super(porps);
this.state = {
movies: []
};
}
componentDidMount() {
// 发起ajax请求获取数据
this.setState({
movies
});
}
render() {
return (
<div>
<MovieList movies={this.state.movies}> </MovieList>
</div>
);
}
}
export default MovieA;
//AB组件内容一致 这里就拿出一个
MovieList.js
import React, { Component } from "react";
class MovieList extends Component {
render() {
return (
<div>
<ul>
{this.props.movies.map((item, i) => {
return <li key={i}>{item.title}</li>;
})}
</ul>
</div>
);
}
}
export default MovieList;
但是注意,随着项目的越来越大,数据的增加,这两种类别显然是无法满足项目。如果说有100种电影种类难道要写100个这样组件?呐在Vue中我们使用mixin
进行混入,在React中我们可以使用高阶组件。
我新建立了一个文件夹HOC专门存放高阶组件,并且新建了一个文件WithFetch.js
import React, { Component } from "react";
export const WithFetch = fetch => Comp => {
return class extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
componentDidMount() {
//模拟数据来源
if (fetch === "A") {
this.setState({
data: [
{
id: 1,
title: "《除日本岛以外全部沉没》",
category: "A"
},
{
id: 2,
title: "《白给秀》",
category: "A"
}
]
});
} else {
this.setState({
data: [
{
id: 1,
title: "《上海堡垒》",
category: "B"
},
{
id: 2,
title: "《甜蜜暴击》",
category: "B"
}
]
});
}
}
render() {
return <Comp {...this.props} data={this.state.data}></Comp>;
}
};
};
MovieA.js
import React, { Component } from "react";
import MovieList from "./MovieList";
import { WithFetch } from "../HOC/WithFetch";
@WithFetch("A")
class MovieA extends Component {
render() {
return (
<div>
<MovieList movies={this.props.data}> </MovieList>
</div>
);
}
}
export default MovieA;
5.1.3 权限控制
有这样一个场景,一个网站有一些页面或者组件只能admin身份的用户才能访问,普通用户无法访问。
首先我在HOC目录下新建了一个WithAdmin.js
作为判断用户权限的高阶组件,重写了componentDidMount()
生命周期函数,模拟了一个登录状态,且给登录状态赋值,随后render
中如果权限匹配则返回一个组件权限不足返回一个提示组件。
import React, { Component } from "react";
export const WithAdminAuth = Comp => {
return class extends Component {
constructor(props) {
super(props);
this.state = {
isAdmin: false
};
}
componentDidMount() {
// 模拟权限获取
const currentRole = "Admin";
this.setState({
isAdmin: currentRole === "Admin"
});
}
render() {
if (this.state.isAdmin) {
return <Comp> {...this.props}>Admin</Comp>;
} else {
return <div>权限不足</div>;
}
}
};
};
随后我在某一些组件中想给附加一个权限检测,则我就在哪一个组件引用这个高阶组件。
import React, { Component } from "react";
import { WithAdminAuth } from "../HOC/WithAdmin";
@WithAdminAuth
class PageA extends Component {
render() {
return <div></div>;
}
}
export default PageA;
如果权限是Admin则会显示一些组件,写到这里我们的这个组件算是完成了,但是为了让组件更加健壮一点我们还得改造下,毕竟一个网站有可能有很多权限组。所以我们把组件改造成这样。
import React, { Component } from "react";
export const WithAdminAuth = role => Comp => {
return class extends Component {
constructor(props) {
super(props);
this.state = {
isAdmin: false
};
}
componentDidMount() {
// 模拟权限获取
const currentRole = "Admin";
this.setState({
isAdmin: currentRole === role
});
}
render() {
if (this.state.isAdmin) {
return <Comp {...this.props}></Comp>;
} else {
return <div>权限不足</div>;
}
}
};
};
role用来接收参数进行比较,也可以定义多个权限,然后改造这个组件就好了,来一个或判断就搞定了
5.2 高阶组件总结
写了这么多先总结一波我们为什么要用高阶组件
- react高阶组件能够让我们写出更加易于维护的react代码
高阶组件是什么?
- 本质上是函数,这个函数接收一个或者多个组件,返回一个新组件
如何实现高阶组件
- 属性代理是最常见的方式
- 反向继承
6 组件通信 Context
Context提供了一个无需为每层组件手动添加props,就能在组件树间进行数据传递的方法。在一个典型的React应用中,数据是通过props属性自上而下(由父到子)进行传递的,但这种做法对于某些属性而言是极其繁琐的(例如:地区偏好,UI主题),这些这些属性是应用程序中许多组件都需要的。Context提供了一种组件间共享此类值的方式,而不必显示的通过组件树的逐层传递props
6.1 何时使用Context
Context设计是为了共享那些全局属性,例如当前认证的用户、主题等。举个例子:
import React, { Component } from "react";
import { Button } from "antd";
const ThemeContext = React.createContext();
class ThemeBtn extends Component {
constructor(props) {
super(props);
}
// 首先定制当前创建的上下文对象 为当前实例的静态属性
// 在渲染的方法中使用 this.context获取共享数据
static contextType = ThemeContext;
render() {
return <Button type={this.context.type}>{this.context.name}</Button>;
}
}
function Toolbar(props) {
return <ThemeBtn></ThemeBtn>;
}
// Context === Vue的provide和inject 在React中用的是provider和consumer
class ContextSimple extends Component {
constructor(props) {
super(props);
this.state = {
stroe: {
type: "primary",
name: "按钮"
}
};
}
render() {
return (
<div>
<ThemeContext.Provider value={this.state.stroe}>
<Toolbar></Toolbar>
</ThemeContext.Provider>
</div>
);
}
}
export default ContextSimple;
但是这样选题略显复杂,还有一种相对简便的函数式渲染方式
import React, { Component } from "react";
import { Button } from "antd";
const ThemeContext = React.createContext();
class ThemeBtn extends Component {
constructor(props) {
super(props);
}
// static contextType = ThemeContext;
render() {
// return <Button type={this.context.type}>{this.context.name}</Button>;
return (
<ThemeContext.Consumer>
{value => {
return <Button type={value.type}>{value.name}</Button>;
}}
</ThemeContext.Consumer>
);
}
}
function Toolbar(props) {
return <ThemeBtn></ThemeBtn>;
}
// Context === Vue的provide和inject 在React中用的是provider和consumer
class ContextSimple extends Component {
constructor(props) {
super(props);
this.state = {
stroe: {
type: "primary",
name: "按钮"
}
};
}
render() {
return (
<div>
<ThemeContext.Provider value={this.state.stroe}>
<Toolbar></Toolbar>
</ThemeContext.Provider>
</div>
);
}
}
export default ContextSimple;
但是注意!这样进行传值他的属性名必须为value否则报错
虽然第二种方式比第一种好理解了一些但是可以想象如果一个更加复杂的项目你需要创建多个这样的上下文,这样仍然很复杂,解决的方案就是嵌入Redux插件它类似于Vue的VueX这个放到以后去聊。
6.2组件通信高阶组件装饰器写法
编写的过程中我们不难发现有很多的东西都是公有的,所以我们可以写两个高阶组件一个提供者一个消费者。
首先新建一个index.js在HOC目录下
import React, { Component } from "react";
const ThemeContext = React.createContext();
//提供者
export const withPriovider = Comp => {
return class extends Component {
constructor(props) {
super(props);
// 共享的数据
this.state = {
stroe: {
type: "primary",
name: "按钮"
}
};
}
render() {
return (
<ThemeContext.Provider value={this.state.stroe}>
<Comp {...this.props}></Comp>
</ThemeContext.Provider>
);
}
};
};
//消费者
export const withConsumer = Comp => {
return class extends Component {
render() {
return (
<ThemeContext.Component>
{value => <Comp {...this.props} value={value}></Comp>}
</ThemeContext.Component>
);
}
};
};
在使用的时候
import React, { Component } from "react";
import { Button } from "antd";
import { withPriovider, withConsumer } from "../HOC/index";
@withConsumer
class ThemeBtn extends Component {
render() {
return (
<Button type={this.props.value.type}>{this.props.value.name}</Button>
);
}
}
function Toolbar(props) {
return <ThemeBtn></ThemeBtn>;
}
@withPriovider
class ContextSimple extends Component {
render() {
return <Toolbar></Toolbar>;
}
}
export default ContextSimple;
结束
在这一个部分把React的最核心的内容高阶组件写出来了顺便还写了一些按需引入的方式渲染优化等等。。。至此React高阶组件部分结束,接下来就是React全家桶的部分,全家桶写完就可以做项目了。
首发于:F_picachoの领域
同步发布于:掘金
作者:F_picacho
技术指导:我的两位师傅 @刘建伟 @Mjj
感谢:@白菜 对Blog的运营以及维护
文档下载:https://ww.lanzous.com/ictlede
2020/05/20