React Portals与Error Boundaries

简介: 在16.x版本之后React提供了2个革新性的特性——Portals和Error Boundaries。Portals彻底解决了模式对话框不在根节点出现的问题,并能很好的合并到React组件结构中来。在16.x版本React调整了异常处理的方式,结合Error Boundaries特性能够更好的捕获处理各种问题。

Portals

在React 16.x 新增了一个名为“Protals”的特性,直接按照字面意思翻译实在不靠谱。在描述这个特性时,我们还是用官方的英文单词来指定它。Portals的作用简单的说就是为了便于开发“弹窗”、“对话框”、“浮动卡片”、“提示窗”等脱离标准文档流的组件而设定的,用于替换之前的unstable_renderSubtreeIntoContainer。 

15.x之前的时代实现"弹窗"

过去没有这个特性的时候,我们使用React绘制“弹窗”之前无非就三种方法:

1.将弹窗作为一个子元素在组件中直接使用,然后赋予弹窗 {position: fixed ,z-index:99}这样的样式,让他漂浮在整个页面应用的最上层并相对与整个浏览器窗口定位。如果你认为fixed能实现所有要求,那么最好把下面的这个页面代码复制到本地运行看看:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>Fixed</title>
</head>
<body>
<div class="top-div">
    <div class="fixed-div">Do I look fixed to you?</div>
</div>
</body>
<style>
    .top-div {
        width: 300px;
        height: 300px;
        background: coral;
        transform: translate(100px, 100px);
        animation: diagonal-loop 1s infinite alternate;
    }
    .fixed-div {
        position: fixed;
        background: rgba(0, 0, 0, 0.7);
        width: 100%;
        height: 100%;
        top: 100px;
        left: 100px;
        padding: 10px;
        color: white;
    }
    @keyframes diagonal-loop {
        0% {
            transform: translate(100px, 100px);
        }
        100% {
            transform: translate(200px, 200px);
        }
    }
</style>
</html>

除此之外,这种方式处理事件的冒泡也会导致一些问题。

2.使用unstable_renderSubtreeIntoContainer方法将弹窗组件添加到body中。官方文档明确告诉你了,这玩意是有坑的,使用起来也到处是雷区。

3.最后一种方式是使用Redux来全局控制,可以在React中的模式对话框一文了解使用Redux实现对话框的内容。虽然能解决前面2个问题,但是使用 Redux 除了多引入一些包之外,这也不是一种很“自然”的实现方式。

Protals的使用

Protals组件的使用方式和普通的React组件并没有太大差异,只不过要用一个新的方法将其包裹起来:

/**
* @param child 需要展示在Protals中的组件,如<div>child</div>
* @param container 组件放置的容器,就是一个Element对象。例如 document.getElementById('pop');
*/
ReactDOM.createPortal(child, container)

通常情况下,我们需要为某个组件增加子元素都会直接写在render()方法中:

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

而如果是一个 Protals 特性的组件,我们通过下面的过程创建它:

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

Protals的事件传递

Protals特性的组件渲染成真实DOM后结构上和虚拟DOM不完全一致,但是其事件流还是像普通的React组件一样可以在父组件中接收并加以处理。所以我们依然可以按照冒泡的方式处理Protals组件的事件。

看个代码的例子,我们定义两个组件——AppPop

App是整个页面的框架,负责将Pop弹窗中输入的内容显示到页面中。React 会将弹窗直接添加为<body>的子元素。

class App extends React.Component {
    //constructor 
    clickHandle() {
        this.setState({popShow: true})
    }
    submitHandle(value) {
        this.setState({message: value, popShow: false})
    }
    cancelHandle() {
        this.setState({popShow: false})
    }
    render() {
        return (
            <div>
                <p>Input Message : {this.state.message}</p>
                <button onClick={this.clickHandle}>Click</button>
                {this.state.popShow && 
                <Modal>
                    <Pop onSubmit={this.submitHandle} onCancel={this.cancelHandle}/>
                </Modal>}
            </div>
        )
    }
}
class Pop extends React.Component {
    //constructor
    submitHandle() {
        this.props.onSubmit(this.el.value)
    }
    render() {
        const {onCancel} = this.props
        return createPortal(
            <div>
                <div><span onClick={onCancel}>X</span></div>
                <textarea ref={ref=>this.el=ref}/>
                <div>
                    <button onClick={this.submitHandle}>submit</button>
                    <button onClick={onCancel}>cancel</button>
                </div>
            </div>,
            document.getElementById('body'))
    }
}

以上只是示例,已实现的源码在:https://github.com/chkui/ReactProtalExample。你可以执行下面这几步运行,并在浏览器输入http://localhost:8080/看到效果。

$ git clone https://github.com/chkui/ReactProtalExample.git
$ npm install #按照node_module
$ npm start #运行webpack

观察代码我们会发现:实现这个弹窗的效果仅仅需要在旧的React组件编码的方式上增加一层createPortal 方法包装即可。其他的处理方式没有任何变化。但是出现弹窗后,观察真实的DOM结构,你会发现弹窗是出现在<body />标签下,脱离了React的树形结构:

<body id="body">
    <div id="root">
      <div class="app">
         <p class="message">Input Message : Input</p>
         <button class="button">Click</button>
      </div>
    </div>
    <div class="modal"> <!-- 弹窗的DOM -->
      <div class="mask"></div>
      <div class="pop">
         <div class="title"><span class="close">X</span></div>
         <textarea class="text" placeholder="input message"></textarea>
         <div class="pop-bottom">
             <button class="button pop-btn">submit</button>
             <button class="button pop-btn">cancel</button>
         </div>
      </div>
    </div>
</body>

Error Boundaries

在 16.x 版本之前,React并没有对异常有什么处理(15.x 增加的 unstable_handleError 满地是坑),都是让使用React的开发人员按照标准JavaScript的方式自行处理可能会出现的异常,这会导致某些由底层渲染过程引起的异常很难定位。此外,由于一个React组件常常伴随多个生命周期方法(lifecycle methods),如果要全面的去处理异常,会导致代码结构越来越差。

为了解决这些坑,最新版本的React提供了一个优雅处理渲染过程异常的机制—— Error Boundaries 。同时,随着 Error Boundaries 的推出,React也调整了一些异常处理的的行为和日志输出的内容。

Error Boundaries特点

特点1:通过一个生命周期方法捕获子组件的所有异常:

/**
*@param error: 被抛出的异常
*@param info: 包含异常堆栈列表的对象
**/
componentDidCatch(error, info)

特点2:只能捕获子组件的异常,而不能捕获自身出现的异常。

特点3:只能捕获渲染方法,和生命周日方法中出现的异常。而事件方法中的异常、异步代码中的异常(例如setTimeoout、一些网络请求方法)、服务端渲染时出现的异常以及componentDidCatch方法中出现的异常是无法被捕获的。如果需要捕获这些异常,只能使用JavaScripttry/catch语法。

异常处理行为变更

16.x 之后的React的异常处理较之前有一些变动。当组件在使用的过程中出现某个异常没有被任何 componentDidCatch 方法捕获,那么 React 将会卸载掉整个 虚拟Dom树。这样的结果是任何未处理的异常都导致用户看到一个空白页面。官方的原文——“As of React 16, errors that were not caught by any error boundary will result in unmounting of the whole React component tree”。

这样的目的是尽可能保证页面完整性,避免由于页面的错误而导致业务逻辑错误。所以React升级到16.x版本后,至少在最顶层的根节点组件实现 componentDidCatch 方法并附加一个 错误提示的简单组件。如果根节点的组件需要处理的事物太复杂,最好多加一层包装组件仅处理异常。

有了 componentDidCatch 之后,我们可以更细粒度的按照模块或者业务来控制异常。还可以专门设定一个服务器接口来收集页面在客户端运行时出现的异常。

优化异常堆栈

新版本的React优化了异常输出,能够更清晰的跟踪到出错的位置。异常日志输出的内容将会比之前的React丰富很多,除了输出JavaScript的异常信息,还会清晰的定位到错误出现的组件:

如果你的项目使用最新版本的 create-react-app 创建的,那么这一项功能已经存在了。如果没使用 Create React App,那么可以通过一个 Babel 的插件添加这个功能:

$ npm install --save-dev babel-plugin-transform-react-jsx-source

然后在对应的配置(.babelrcwebpack的plugins等)中添加:

{
  "plugins": ["transform-react-jsx-source"]
}

切记这项功能仅仅用于开发或测试环境,切勿用于生产环境。某些浏览器可能不支持 Function.name  的属性,可能无法正确显示组件名称(例如所有版本的IE)。可以通过使用一些 polyfill 来解决这个问题,比如这个 function-name工具 。

代码实例

最后是一个代码的例子。请按照以下步骤到github上clone下来运行。

$ git clone https://github.com/chkui/ErrorBoundariesExample.git #下载代码
$ npm install #安装node_module
$ npm start #安装完后webpakc启动

例子值得关注的就几个点。

1.通过 webpack 的方式引入了babel的源码映射插件用以定位异常出现的位置。

module: {
        rules: [{
            test: /\.js$/,
            use: [{
                loader: 'babel-loader',
                options: {
                    presets: ['es2015', 'stage-0', 'react'],
                    plugins: ['transform-react-jsx-source'], //添加插件
                }
            }],
            exclude: /node_modules/
        }]
    },

2.定义了四个组件——AppParentChildErrorTip,分别是入口组件、父组件、子组件和捕获到异常时用来提示的组件。

class App extends React.Component {
    //constructor
    componentDidCatch(error, info) {
        this.setState({error: true}) //处理子组件的异常
    }
    render() {
        return (<div className="app">
                <h2>Example</h2>
                {this.state.error ? (<ErrorTip />) : (<Parent/>)}
            </div>)
    }
}
class Parent extends React.Component {
    //constructor
    clickHandle() {
        try {
            throw new Error('event error')
        } catch (e) {
            this.setState({myError: true})
        }
    }
    childErrorClickHandle(){
        this.setState({childError:true})
    }
    componentWillUpdate(nextProps, nextState) {
        if (nextState.myError) {
            throw new Error('Error')
        }
    }
    componentDidCatch(error, info) {
        this.setState({catchError: true})
    }
    render() {
        return (
            <div className="box">
                <p>Parent</p>
                <button onClick={this.clickHandle}>throw parent error</button>
                <button onClick={this.childErrorClickHandle}>throw child error</button>
                {this.state.catchError ? (<ErrorTip/>):(<Child error={this.state.childError}/>)}
            </div>
        )
    }
}
class Child extends React.Component{
    //constructor
    componentWillReceiveProps(nextProps){
        if(nextProps.error){throw new Error('child error')}
    }
    render(){
        return (<div className="box">
            <p>Child</p>
        </div>)
    }
}

Child抛出的异常会被Parent组件处理、Parent组件抛出的异常会被App组件处理,组件无法捕获自生出现的异常。

最后,由于16.x版本提供了componentDidCatch的功能,所以将15.x的unstable_handleError特性取消调了,如果需要进行升级的可以去 这里 下载并使用升级工具。

相关文章
|
2月前
|
前端开发 JavaScript 测试技术
React 错误边界 (Error Boundaries) 详解
【10月更文挑战第17天】在现代前端开发中,React 通过“错误边界”机制提高了应用的健壮性和用户体验。错误边界是一种特殊的 React 组件,能捕获并处理其子组件树中的 JavaScript 错误,防止应用因局部错误而整体崩溃。创建错误边界需实现 `static getDerivedStateFromError` 和 `componentDidCatch` 方法,分别用于更新状态和记录错误。正确使用错误边界,可以有效提升应用的稳定性和用户满意度。
217 62
|
前端开发 JavaScript 容器
React Portals
React Portals
129 0
React-其它内容-Portals 和 React-父子组件通讯-类组件
React-其它内容-Portals 和 React-父子组件通讯-类组件
55 0
|
数据采集 前端开发 测试技术
React项目中Manifest: Line: 1, column: 1, Syntax error的解决方法
大家好,今天和大家分享一个React项目中的一个小报错的解决方法。 在创建了一个项目后会有几个文件
|
前端开发
react 组件进阶之Error Boundaries(错误边界)
错误边界: 用来捕捉错误的代码,说到捕捉错误。大家可能都会说 直接try catch 不就行了。对的,try catch 确实是一种在各个语言比较通用的方法。但是在react 组件中,如果某一个组件发生错误,他是会往他的父级组件抛出错误的,然后自己是会被卸载的。如果到跟组件都不能够处理错误,这个组件树就会被卸载,组件树卸载导致的页面效果就是直接的报错。
react 组件进阶之Error Boundaries(错误边界)
|
JavaScript 前端开发 容器
react 组件进阶之 Portals
Portals 翻译为门户,但是感觉不对。作用类似插槽,但是不是vue 里面的插槽哦,有点像vue3 里面的 teleport,两者的作用都是将对于的虚拟dom 插入到真实dom的某个位置上。这里不讲两者的区别,只讲 Portals 的用法
react 组件进阶之 Portals
|
缓存 JSON 前端开发
《精通react/vue组件设计》之配合React Portals实现一个功能强大的抽屉(Drawer)组件
本文是笔者写组件设计的第六篇文章,内容依次从易到难,今天会用到react的高级API React Portals,它也是很多复杂组件必用的方法之一. 通过组件的设计过程,大家会接触到一个完成健壮的组件设计思路和方法,也能在实现组件的过程逐渐对react/vue的高级知识和技巧有更深的理解和掌握,并且在企业实际工作做游刃有余.
321 0
|
资源调度 前端开发 Java
React Native 运行报错 error in opening zip file
基于 React Native 中文网教程 编译并运行 React Native 应用,在项目 (AwesomeProject) 根目录下运行命令 yarn rect-native run-android 或 yarn android 出现如下错误。
986 0
|
7月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
382 0
|
7月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
131 0