前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React 组件化开发(一)

React 组件化开发(一)

作者头像
一粒小麦
发布2019-07-18 17:55:14
2.4K0
发布2019-07-18 17:55:14
举报
文章被收录于专栏:一Li小麦一Li小麦

本文主要内容

  • 第三方组件的使用方法
  • 自定义组件
  • 组件化实现技术
  • 高阶组件

写react和传统的js差不多。只是有一个设计思想贯穿了整个框架。用一个公式来表达就是:

代码语言:javascript
复制
// 状态机模型
UI=f(state)

AntD

在国内最出名的react组件库就是antD了。

官方文档:https://ant.design/index-cn

代码语言:javascript
复制
npm install antd --save

在组件中可以这么用:

代码语言:javascript
复制
import React, { Component } from 'react'
import Button from 'antd/lib/button'
import 'antd/dist/antd.css'

export default class Test extends Component {
    render() {
        return (
            <div>
                <Button>aaa</Button>
            </div>
        )
    }
}

这是一种低效且lowB的方式:用起来繁琐,加载的是整个antd的css。实际生产中需要做按需加载。

局部css

上讲留了一个坑,就是css全局污染问题。如何避免?

代码语言:javascript
复制
npm run eject

// react-scripts eject

执行后多了一个config文件夹,可以获得完整的webpack.config.js的控制权。

然后以 webpack.config.dev.js为例,找到:

代码语言:javascript
复制
// "postcss" loader applies autoprefixer to our CSS.
      // "css" loader resolves paths in CSS and adds assets as dependencies.
      // "style" loader turns CSS into JS modules that inject <style> tags.
      // In production, we use a plugin to extract that CSS to a file, but
      // in development "style" loader enables hot editing of CSS.
      {
        test: /\.css$/,
        loader: 'style!css?importLoaders=1!postcss'
      },

postcss loader 将autoprefixer应用于CSS。 css loader 解析css中的路径并将静态资源作为依赖项添加。 style loader 将CSS转换为注入 <style>标记的JS模块。 在生产环境中,我们使用插件将该CSS提取到文件中,但是 在开发环境下,style loader启用CSS的热编辑。

做如下修改:

代码语言:javascript
复制
{
        test: /\.css$/,
        loader: "style-loader!css-loader?modules"
      },

上面代码中,关键的一行是 style-loader!css-loader?modules,它在 css-loader后面加了一个查询参数 modules,表示打开 CSS Modules 功能。

使用时:

代码语言:javascript
复制
/* App.module.css */
.aaa {
  color: red
}
.bbb{
  color: blue
}

然后导入:

代码语言:javascript
复制
import style from './App.module.css'

// ...
    render() {
        return (
            <div className={style.aaa} >
                111<div className={style.bbb}>222</div>
            </div>
        )
    }

即可实现局部应用css

上图中,class为aaa的不会生效。

按需加载

做按需加载用 eject实在是太不优雅了,优雅实现需要引入以下三个依赖

  • react-app-rewired:辅助启动
  • customize-cra:可扩展webpack的配置 ,类似vue.config.js
  • babel-plugin-import
代码语言:javascript
复制
npm install react-app-rewired customize-cra babel-plugin-import -D

在根目录创建 config-overrides.js

代码语言:javascript
复制
// 无损复写webpack配置
// override返回一个函数,以下会成为一个webpack配置对象。
const {override,fixBabelImports}=require('customize-cra')
module.exports=override(
    fixBabelImports('import',{
    libraryName:'antd',
    libraryDirectory:'es',//引入antd组件库下的es下的css
    style:'css'
  })
)


// 源码
const override = (...plugins) => flow(...plugins.filter(f => f));
//flow源码:参数是由函数组成数组,返回一个任意类型的值。转化为webpack配置
flow(...func: Array<Many<(...args: any[]) => any>>): (...args: any[]) => any;


// fixBabelImports源码,库名,配置
const fixBabelImports = (libraryName, options) =>
  addBabelPlugin([
    "import",
    Object.assign(
      {},
      {
        libraryName
      },
      options
    ),
    `fix-${libraryName}-imports`
  ]);

同时修改 package.json

代码语言:javascript
复制
"scripts":{
  "start":"react-app-rewired start",
  "build":"react-app-rewired build",
  "test":"react-app-rewired test",
  "eject":"react-app-rewired eject"
}

那么在应用中可以实现按需加载antd组件:

代码语言:javascript
复制
import {Button} from 'antd'

组件类型

容器组件和展示组件

基本原则:容器组件负责数据获取,展示组件根据props获取信息。

代码语言:javascript
复制
import React, { Component } from "react";

// 展示组件
function Comment({ data }) {
    return (
        <div>
            <p>{data.body}</p>
            <p> --- {data.author}</p>
        </div>)
}


// 容器组件
export default class CommentList extends Component {
    constructor(props) {
        super(props);
        this.state = {
            comments: []
        };
    }
    componentDidMount() {
        setTimeout(() => {
            this.setState({
                comments: [
                    { body: "react is very good", author: "facebook" },
                    { body: "vue is very good", author: "youyuxi" }
                ]
            });
        }, 1000);
    }
    render() {
        return (
            <div>
                {this.state.comments.map((c, i) => (
                    <Comment key={i} data={c} />
                ))}
            </div>
        );
    }
}

这个页面将在1s后打印留言列表。

Comment非常纯粹,毫无业务逻辑。这样写有许多优势:

  • 更小
  • 更专注、重用性高
  • 高可用
  • 易于测试、性能更好。

展示型组件最好用函数实现。

假设页面请求是每隔1s进行的轮询。我们在 Comment组件内打印记录,会很好调试渲染次数。(每秒渲染2次)

PureComponent

再假如,轮询的数据常年不变,开发者并不希望频繁渲染,应该怎么做呢?

固然可以用 shouldComponentUpdate(nextProps)。但是需要一一对原来的数据进行判断。非常繁琐。

先介绍一下 PureComponent,平时我们创建 React 组件一般是继承于 Component,而 PureComponent 相当于是一个更纯净的 Component,对更新前后的数据进行了一次浅比较。只有在数据真正发生改变时,才会对组件重新进行 render。因此可以大大提高组件的性能。

可以把Comment改写为Class形式:

代码语言:javascript
复制
// 父组件直接把props传进去
<Comment key={i} {...c} />
// 展示组件
class Comment extends PureComponent {
    render() {
        console.log('render Comment')
        let {body,author}=this.props;
        return (
            <div>
                <p>{body}</p>
                <p> --- {author}</p>
            </div>)
    }
}

源码解读

PureComponent由15.3版本引入。现在可以阅读一下它的源码

(地址:https://github.com/facebook/react/blob/master/packages/react/src/ReactBaseClasses.js):

代码语言:javascript
复制
function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;

可以看到 PureComponent 的使用和 Component 一致,只时最后为其添加了一个 isPureReactComponent 属性。ComponentDummy 就是通过原型模拟继承的方式将 Component 原型中的方法和属性传递给了 PureComponent。同时为了避免原型链拉长导致属性查找的性能消耗,通过 Object.assign 把属性从 Component 拷贝了过来。

代码语言:javascript
复制
* `PureComponent` implements a shallow comparison on props and state and returns true if any props or states have changed.

它首先继承了普通React.Component的方法,然后shouldComponentUpdate函数被复写:

代码语言:javascript
复制
import is from './objectIs';

const hasOwnProperty = Object.prototype.hasOwnProperty;

function shallowEqual(objA: mixed, objB: mixed): boolean {
  // 先判断引用地址
  if (is(objA, objB)) {
    return true;
  }
    // 判断类型
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }
    // 再看属性长度
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // 浅层比较-出于性能考虑。
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}

export default shallowEqual;

由于比较方式是浅比较(引用地址,key),注意传值方式,值类型或者地址不变的且仅根属性变化的引用类型才能享受该特性。

  • 引用地址不能变(immutable.js)
  • 改变传值方式
React.memo

还是不够优雅,尝试使用memo:

React 16.6.0 使用 React.memo 让函数式的组件也有PureComponent的功能。

代码语言:javascript
复制
// 展示组件=>高阶组件
const Comment=React.memo(data=>{
    console.log('render Comment')
    return (
        <div>
            <p>{data.body}</p>
            <p> --- {data.author}</p>
        </div>)
})

问题来了,memo是什么?

高阶组件

高阶组件(HOC,Higher-Order Components)是React非常重要的扩展组件方式。高阶组件是React中重用组件逻辑的高级技术,它不是react的api,而是一种组件增强模式。高阶组件是一个函数,它返回另外一个组件,产生新的组件可以对被包装组件属性进行包装,甚至重写部分生命周期。

设计组件时,粒度需要尽可能小,同时尽可能复用。但是在非常复杂的情况下,就需要对粒度很小的组件进行处理。这就是高阶组件的诞生背景。在官方文档中更加推荐这种写法,甚于传统的继承写法。

高阶实现

需求:考虑有这样一个aaa组件

代码语言:javascript
复制
function aaa(props){
    return <div>{props.stage}-{props.name}</div>
}

在某种场景下,我需要让stage固定为react,意味着需要对aaa进行进一步封装:

代码语言:javascript
复制
const withStage = (Component) => {
    const NewComponent = (props) => {
        return <Component {...props} stage="react" />;
    };
    return NewComponent;
};

// 导出:
export default withStage(aaa)

那么使用时可以肆意搞起来了:

代码语言:javascript
复制
import Hoc from './withStage'
// ...
    render(){
        return (
            <div>
                <Hoc name="aaa"/>
                <Hoc name="aaabbb"/>
            </div>
        );
  }

渲染结果:

上述withStage组件,代理了Component,只是多传了一个name参数。

name可以用ajax请求获得,或者继承于其它组件。

withRouter也是一个高阶组件。

链式调用

链式调用就是一个中间件。

假如我要做一个日志记录。

代码语言:javascript
复制
import React, { Component } from 'react'
import { Button } from 'antd'
const withHoc = (Component) => {
    const NewComponent = (props) => {
        return <Component {...props} name="my-Hoc" />;
    };

    return NewComponent;
};

const withLog = Component => {
    class NewComponent extends React.Component {
        componentDidMount() {
            console.log('didMount', this.props)
        }

        render() {
            return <Component {...this.props} />;
        }
    }

    return NewComponent
}

class App extends Component {
    render() {
        return (
            <div className="App">
                <h2>hi,{this.props.name}</h2>
                <Button type="primary">Button</Button>
            </div>
        )
    }
}
export default withHoc(withLog(App))
链式调用的装饰器

以上链式调用实现中,这种嵌套其实很难看。es7中支持了一种优秀的写法——装饰器。专门处理这种问题。给webpack装个插件吧:

代码语言:javascript
复制
npm install -D @babel/plugin-proposal-decorators

config-override.js中,

代码语言:javascript
复制
const {addDecoratorsLegacy}=require('customise-cra');

module.exports={
  // ... ,
  addDecaratorsLefacy()
};

在写完withHoc和withLog后:

代码语言:javascript
复制
// ...

// 如果你想,还可以@多次上诉中间件
@withHoc
@withLog

class App extends Component {
    render() {
        return (
            <div className="App">
                <h2>hi,{this.props.name}</h2>
                <Button type="primary">Button</Button>
            </div>
        )
    }
}
export default App

组件复合 - Composition

复合组件使我们以更敏捷的方式定义组件的外观和行为,比起继承的方式它更明确和安全。 ——官方文档

匿名插槽
代码语言:javascript
复制
import React,{Component} from 'react'
// Dialog作为容器不关心内容和逻辑 
function Dialog(props) {
    let {color}=props;
    // children是固定名称(不能改),类似匿名插槽
    return <div style={{ border: `4px solid ${color}` }}>{props.children}</div>;
}

// WelcomeDialog通过复合提供内容 function WelcomeDialog() { 
function WelcomeDialog() {
    return (
        <Dialog color="blue">
            <h1>欢迎光临</h1>
            <p>感谢使用react</p>
        </Dialog>
    );
} 

export default function Composition(){
    return (
        <WelcomeDialog />
    )
}

这是一个类似插槽的功能,容器型组件都可以这么写。

传jsx:具名插槽

Dialog的传值可以设置多个属性,表达式,jsx都可以。

代码语言:javascript
复制
function Dialog(props) {
    let {color,footer}=props;
    return <div style={{ border: `4px solid ${color}` }}>
      {props.children}
      {footer}
    </div>;
}

// WelcomeDialog通过复合提供内容 function WelcomeDialog() { 
function WelcomeDialog() {
    return (
        <Dialog color="blue" footer={ <button>确定</button>}>
            <h1>欢迎光临</h1>
            <p>感谢使用react</p>
        </Dialog>
    );
}

这就像具名插槽。

传方法:作用域插槽
代码语言:javascript
复制
import React,{Component} from 'react'
// Dialog作为容器不关心内容和逻辑 
function Dialog(props) {
    let {color,footer}=props;
    // children是固定名称(不能改),类似插槽
    return( 
        <div style={{ border: `4px solid ${color}` }}>
            {props.children}
            {props.foo('i come from foo')}
            {footer}
        </div>
    )
}

// WelcomeDialog通过复合提供内容 function WelcomeDialog() { 
function WelcomeDialog() {
    return (
        <Dialog color="blue" footer={ <button>确定</button>} foo={(e)=>{return <p>{e}</p>}}>
            <h1>欢迎光临</h1>
        </Dialog>
    );
}
chilren的本质

props.children究竟是什么?

children可以说是jsx。但是:

代码语言:javascript
复制
function Dialog(props) {
    let { color, footer } = props;
    return (
        <div style={{ border: `4px solid ${color}` }}>
            {props.children()}
            {props.foo('i come from foo')}
            {footer}
        </div>
    )
}

function WelcomeDialog() {
    return (
        <Dialog 
          color="blue" 
          footer={<button>确定</button>} 
          foo={(e) => { return <p>{e}</p> }}>
            {e =>(
            <div>
                <p>welcome</p>
            </div>
            )}
        </Dialog>
    );
}

其实children完全可以是一个合法的jsx表达式。

那就可以有很多丰富的操作了。

组件的设计

React.Children.map

需求1 比如说我要设置一个脏话过滤系统:

代码语言:javascript
复制
function Dialog(props) {
    let { color, footer } = props;
    // children是固定名称(不能改),类似插槽
    return (
        <div style={{ border: `4px solid ${color}` }}>
            {props.children()}
            {props.foo('i come from foo')}
            {footer}
        </div>
    )
}

// WelcomeDialog通过复合提供内容 function WelcomeDialog() { 
function WelcomeDialog() {
    return (
        <Dialog color="blue" footer={<button>确定</button>} foo={(e) => { return <p>{e}</p> }}>
            {/* <h1>欢迎光临</h1> */}
            {e =>(
            <div>
                <p>welcome</p>
            </div>
            )}
        </Dialog>
    );
}

function Filter(props){
    return (
        <div>
            {React.Children.map(props.children,child=>{
                console.log(child.props.children.indexOf('uck'))
                if(child.props.children.indexOf('uck')<0){
                    return child
                }
                return false;
            })}
        </div>
    )
}
export default function Composition() {
    return (
        <>
            <WelcomeDialog />
            <Filter>
                <h1>fuck</h1>
                <h2>suck</h2>
                <p>dangjingtao</p>
            </Filter>
        </>
    )
}

所有带uck的脏话都过滤了。map可以对类似数组结构进行遍历。

React.cloneElement

需求2:实现一个RadioGroup的组件,下属有Radio子组件

比如说:

代码语言:javascript
复制
<RadioGroup name="mvvm">
                <Radio value="react">react</Radio>
                <Radio value="vue">vue</Radio>
                <Radio value="Angular">Angular</Radio>
            </RadioGroup>

根据之前的方案,可得:

代码语言:javascript
复制
function RadioGroup(props){
    // 把name赋值给Radio
    return (
        <div style={{border:'1px solid #ccc'}}>
            {React.Children.map(props.children,child=>{
                console.log(child)
                child.props.name=props.name
                return child;
            })}
        </div>
    )
}

function Radio(props){
    return (
        <label htmlFor={`${props.name}`} style={{margin:0}}>
            <input type="radio" name={`${props.name}`} value={`${props.value}`} /> {props.children}
        </label>
    )
}

结果报错。因为虚拟dom是不可扩展的。

不可扩展,只能复制:

代码语言:javascript
复制
function RadioGroup(props){
    // 把name赋值给Radio
    return (
        <div style={{border:'1px solid #ccc'}}>
            {React.Children.map(props.children,child=>{                
                return React.cloneElement(child,{name:props.name});
            })}
        </div>
    )
}

组件跨层级通信——Context

这种模式下有两个角色:

  • Provider:外层提供数据的组件
  • Consumer :内层获取数据的组件
代码语言:javascript
复制
import React, { Component } from 'react'

const FormContext = React.createContext()
console.log(FormContext)

const FormProvider = FormContext.Provider
const FormConsumer = FormContext.Consumer


const store = {
    name: 'djtao',
}

class ContextTest extends Component {
    render() {
        return (
            <FormProvider value={store}>
                <FormConsumer>
                    {store => <p>{store.name}</p>}
                </FormConsumer>
            </FormProvider >
        )       
    }
}
export default ContextTest;
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • AntD
    • 局部css
      • 按需加载
      • 组件类型
        • 容器组件和展示组件
          • PureComponent
            • 源码解读
              • React.memo
              • 高阶组件
                • 高阶实现
                  • 链式调用
                    • 链式调用的装饰器
                    • 组件复合 - Composition
                      • 匿名插槽
                        • 传jsx:具名插槽
                          • 传方法:作用域插槽
                            • chilren的本质
                            • 组件的设计
                              • React.Children.map
                                • React.cloneElement
                                • 组件跨层级通信——Context
                                相关产品与服务
                                容器服务
                                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档