资讯专栏INFORMATION COLUMN

使用 Portal 优雅实现“浮”在页面上的组件

jk_v1 / 949人阅读

摘要:产品需求产品需求,实现一个选择器组件,要求浮在页面上方。本文讨论的主要是,在有类似于组件一样,浮在页面的组件时,如何设计组件树方案一组件是组件的子组件。优势的显示状态属于节点控制,状态管理成本低。包括,事件冒泡。

产品需求

产品需求,实现一个选择器 Selector 组件,要求浮在页面上方。在网上随便找了个图,如下:

实现方案

实现这一的一个 Selector 组件并不难,不是本文的讨论内容。
本文讨论的主要是,在有类似于 Selector 组件一样,“浮”在页面的组件时,如何设计 React 组件树?

方案一:Seletor 组件是 App 组件的子组件。

优势:Selector 属于 App 的子节点,子节点不受父节点的样式属性( position overflow )的干扰。

劣势:Selector 的显示状态属于 App 节点,跨分支传递状态成本太高。使用 Redux 或 Mobx 跨分支传递状态,依赖第三方组件,不利于复用;而手动传递,至少要 4 个步骤,如果 Button 节点更深,步骤会更多。并且这样写出的代码,耦合性太强,不利于维护。

方案二:Selector(fixed) 组件是 Button 组件的子组件。

优势:Selector 的显示状态属于 Button 节点控制,状态管理成本低。

劣势:Selector 属于 Button 的子节点。而当父节点 Button 有文字超出隐藏的需求时(overflow: hidden),子节点 Selector 会被隐藏。

那么,有没有两全齐美的方案呢?有。

方案三:在 React 组件树设计上,Selector 是 Button 的子组件。但是在 DOM 树的角度 Selector 是 Body 的子节点。

在这个方案中,Button 和 Selector 还是属于 React 组件树中的父子节点,享有父子组件状态传递方便的优势。
但是,Button 和 Selector 不再属于 DOM 树中的父子节点!Selector 被渲染到了 Body 节点下面,属于 Body 的子节点。这样 Selector 组件再也不会受到 Button 组件的样式干扰了。

在 React 中如何做到这一点呢?使用 React 16 的 Portals。
这个新属性的介绍文章很短,我就翻译下一吧。翻译只是意译,只为更好理解。

Portals

Portals 提供了一种超级棒的方法,可以将 react 子节点的 DOM 结构,渲染到 react 父节点之外的 DOM 中。

ReactDOM.createPortal(child, container)

第一个参数 child 是任何可以被渲染的 ReactChild,比如 element, string 或者 fragment. 第二个参数 container 是 一个 DOM 元素。

使用方法

一般来说,在 react 中是父子节点的关系,那么在 DOM 中也是父子节点的关系。

render() {
  // 在 react 中 div 和 children 是父子的关系,在 DOM 中 div 和 children 也是父子的关系。
  return (
    
{this.props.children}
); }

然而,有时候打破了这种 react 父子节点和 DOM 父子节点的映射关系是非常有用的。使用 createPortal 可以将 react 的子节点插入到不同的 DOM 节点中。

render() {
  // React 并没有创建一个新的 div,来包裹 children。它将 children 渲染到了 domNode 中。
  // domNode 可以是任意一个合法的 DOM 节点,无论它在 DOM 节点中的哪个位置。
  return ReactDOM.createPortal(
    this.props.children,
    domNode,
  );
}

portal 一个典型的用法是,当父组件有 overflow: hidden 或者 z-index 样式时,但是子组件需要“打破”父组件容器,显示在父组件之外。比如 dialogs,hovercards,tooltips 组件。

[在 CodePen 上尝试一下(https://codepen.io/gaearon/pe...

Portals 的事件冒泡

虽然 portal 可以在 DOM 树中的任意位置,但是它的行为依旧和普通的 React child 一样。比如上下文环境完全一样,无论 child 是不是 portal; portal 也一直存在于在 React 树上,无论它位于 DOM 树中的什么位置。

包括,事件冒泡。portal 节点的事件会冒泡到它的 React 树的祖先节点上,即使这些 React 树上的祖先节点并不是 DOM 树上的祖先节点。比如,有下面的 HTML 结构。


  
    

在 DOM 树中是 portal 和它的 React 父组件兄弟节点,但是由于 React 的事件处理规则,让 portal 的 React 父组件有能力捕获 portal 的冒泡事件。

// These two containers are siblings in the DOM
const appRoot = document.getElementById("app-root");
const modalRoot = document.getElementById("modal-root");

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement("div");
  }

  componentDidMount() {
    // The portal element is inserted in the DOM tree after
    // the Modal"s children are mounted, meaning that children
    // will be mounted on a detached DOM node. If a child
    // component requires to be attached to the DOM tree
    // immediately when mounted, for example to measure a
    // DOM node, or uses "autoFocus" in a descendant, add
    // state to Modal and only render the children when Modal
    // is inserted in the DOM tree.
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    modalRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el,
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {clicks: 0};
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // This will fire when the button in Child is clicked,
    // updating Parent"s state, even though button
    // is not direct descendant in the DOM.
    this.setState(prevState => ({
      clicks: prevState.clicks + 1
    }));
  }

  render() {
    return (
      

Number of clicks: {this.state.clicks}

Open up the browser DevTools to observe that the button is not a child of the div with the onClick handler.

); } } function Child() { // The click event on this button will bubble up to parent, // because there is no "onClick" attribute defined return (
); } ReactDOM.render(, appRoot);

[在 CodePen 上尝试一下(https://codepen.io/gaearon/pe...

父组件能够捕获 portal 的冒泡事件的设计,允许开发者更加灵活的进行抽象,而这些抽象不依赖于 portal 。例如,如果你渲染一个 组件,它的父组件能够捕获它的事件,无论使用的是不是 portal 实现的 (fixed 也能实现)。

使用 portals 的实现 Selector
// 数据和选中的元素的状态由 Selector 自己控制
// 不要将 data、index 状态暴露给其他组件
// 暴露给父组件,越多和父组件耦合的就越重
class Selector extends Component {
    componentDidMount(){
        fetch("xxx")
            .then(data => {
                this.setState({
                    data,
                })
            })
    }

    handleSelect = index => {
        this.setState({
            index
        })
    }
    
    render() {
        return (
            
        )
    }

}

// 控制 Modal 显示状态都封装在 Button 中
class Button extends Component {
    handleClick = () => {
        this.setState( prevState => ({
            show: !prevState.show
        }))
    }
    
    render() {
        return (
            
我是按钮 // 为了保存 Selector 的状态,不要 unmount Modal,用 display: none 实现隐藏。
) } } class App extends Component { render() { return (
) } }
讨论:属性暴露的越多越好,还是越少越好?

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90241.html

相关文章

  • ELSE 技术周刊(2017.10.16期)

    摘要:前端中的计算机领域的通常认为起源于。并对其主要内容作了自己的解读。搬到另一个地区会导致名气降低。年度报告,年最受欢迎的编程语言年上最流行的种编程语言及前十最火热的项目排行榜,分别由及登顶。技术周刊由小组出品,汇聚一周好文章,周刊原文。 showImg(https://segmentfault.com/img/bVWHC4?w=1000&h=710); 本期推荐 反击爬虫,前端工程师的脑...

    0xE7A38A 评论0 收藏0
  • 漫谈 React 组件库开发(一):多层嵌套弹层组件

    摘要:引言组件中有很多弹出式组件,常见的如,以及等。这样一种层次结构在实践中大大降低了各类弹层组件的实现和维护成本。但是的组件实现了一个大多数组件库都没有实现的功能弹层的嵌套处理。 引言 UI 组件中有很多弹出式组件,常见的如 Dialog,Tooltip 以及 Select 等。这些组件都有一个特点,它们的弹出层通常不是渲染在当前的 DOM 树中,而是直接插入在 body (或者其它类似的...

    warmcheng 评论0 收藏0
  • 当大多数人对Vue理解到炉火纯青的时候,是不是该思考一下怎么让vue页面骚气起来

    写在前面 当大多数人Vue理解的炉火纯青的时候,你应该思考怎么让vue页面骚气起来,下面就我个人在接触Vue两年的时间里,在实际工作中门户网站在前端页面交互应用和技巧,炒几道小菜给大家分享一哈,我把它封装成一个项目vue-portal-webUI(github源码),不敢说是UI,但也是各种常见常遇到的情景吧,看懂代码需要一些vue、axios、es6、scss基础、数据基本上是mock,功能和场...

    lingdududu 评论0 收藏0
  • 微前端改造初探

    摘要:我们继续沿用了原来就有的,借此把融入整个微前端框架,而已经改造的则不需要我们的开发团队,分框架组和各个业务组。项目该项目是整个微前端项目的入口。本坑实践它很大的理由也是用自己的方法初探微前端实践方法的可行性。 在写这篇文章的一个多月前,本坑还不知道微前端是什么,大概从字面上的含义是比较小的前端项目。 本坑开始实践它,是由于工作要求。改造一个运行多年,前端用jsp写的服务平台项目(以下简...

    KunMinX 评论0 收藏0
  • 02.react进阶指南

    摘要:指定读取当前的。它为其后代元素触发额外的检查和警告。严格模式检查仅在开发模式下运行它们不会影响生产构建。作用识别不安全的生命周期关于使用过时字符串的警告关于使用废弃的方法的警告检测意外的副作用检测过时的为高阶组件。 react 进阶 懒加载 React.lazy函数能让你像渲染常规组件一样处理动态引入(的组件)。Suspense加载指示器为组件做优雅降级。fallback属性接受任何在...

    zzbo 评论0 收藏0

发表评论

0条评论

jk_v1

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<