前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >这就是你日思夜想的 React 原生动态加载

这就是你日思夜想的 React 原生动态加载

作者头像
政采云前端团队
发布于 2020-05-28 14:29:12
发布于 2020-05-28 14:29:12
2.8K00
代码可运行
举报
文章被收录于专栏:采云轩采云轩
运行总次数:0
代码可运行

React.lazy 是什么

随着前端应用体积的扩大,资源加载的优化是我们必须要面对的问题,动态代码加载就是其中的一个方案,webpack 提供了符合 ECMAScript 提案 (https://github.com/tc39/proposal-dynamic-import) 的 import()语法 (https://www.webpackjs.com/api/module-methods#import-) ,让我们来实现动态地加载模块(注:require.ensure 与 import() 均为 webpack 提供的代码动态加载方案,在 webpack 2.x 中,require.ensure 已被 import 取代)。

在 React 16.6 版本中,新增了 React.lazy 函数,它能让你像渲染常规组件一样处理动态引入的组件,配合 webpack 的 Code Splitting,只有当组件被加载,对应的资源才会导入 ,从而达到懒加载的效果。

使用 React.lazy

在实际的使用中,首先是引入组件方式的变化:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 不使用 React.lazy
import OtherComponent from './OtherComponent';
// 使用 React.lazy
const OtherComponent = React.lazy(() => import('./OtherComponent'))

React.lazy 接受一个函数作为参数,这个函数需要调用 import() 。它需要返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。

图片

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// react/packages/shared/ReactLazyComponent.js
 export const Pending = 0;
 export const Resolved = 1;
 export const Rejected = 2;

在控制台打印可以看到,React.lazy 方法返回的是一个 lazy 组件的对象,类型是 react.lazy,并且 lazy 组件具有 _status 属性,与 Promise 类似它具有 Pending、Resolved、Rejected 三个状态,分别代表组件的加载中、已加载、和加载失败三种状态。

需要注意的一点是,React.lazy 需要配合 Suspense 组件一起使用,在 Suspense 组件中渲染 React.lazy 异步加载的组件。如果单独使用 React.lazy,React 会给出错误提示。

图片

上面的错误指出组件渲染挂起时,没有 fallback UI,需要加上 Suspense 组件一起使用。

其中在 Suspense 组件中,fallback 是一个必需的占位属性,如果没有这个属性的话也是会报错的。

接下来我们可以看看渲染效果,为了更清晰的展示加载效果,我们将网络环境设置为 Slow 3G。

图片

组件的加载效果:

图片

可以看到在组件未加载完成前,展示的是我们所设置的 fallback 组件。

在动态加载的组件资源比较小的情况下,会出现 fallback 组件一闪而过的的体验问题,如果不需要使用可以将 fallback 设置为 null。

当然针对这种场景,React 也提供了对应的解决方案,在 Concurrent Mode (https://react.docschina.org/docs/concurrent-mode-intro.html) 模式下,给 Suspense 组件设置 maxDuration 属性,当异步获取数据的时间大于 maxDuration 时间时,则展示 fallback 的内容,否则不展示。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 <Suspense 
   maxDuration={500} 
   fallback={<div>抱歉,请耐心等待 Loading...</div>}
 >
   <OtherComponent />
   <OtherComponentTwo />
</Suspense>

:需要注意的一点是 Concurrent Mode 目前仍是试验阶段的特性,不可用于生产环境

Suspense 可以包裹多个动态加载的组件,这也意味着在加载这两个组件的时候只会有一个 loading 层,因为 loading 的实现实际是 Suspense 这个父组件去完成的,当所有的子组件对象都 resolve 后,再去替换所有子组件。这样也就避免了出现多个 loading 的体验问题。所以 loading 一般不会针对某个子组件,而是针对整体的父组件做 loading 处理。

以上是 React.lazy 的一些使用介绍,下面我们一起来看看整个懒加载过程中一些核心内容是怎么实现的,首先是资源的动态加载。

Webpack 动态加载

上面使用了 import() 语法,webpack 检测到这种语法会自动代码分割。使用这种动态导入语法代替以前的静态引入,可以让组件在渲染的时候,再去加载组件对应的资源,这个异步加载流程的实现机制是怎么样呢?

话不多说,直接看代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
__webpack_require__.e = function requireEnsure(chunkId) {
    // installedChunks 是在外层代码中定义的对象,可以用来缓存了已加载 chunk
  var installedChunkData = installedChunks[chunkId]
    // 判断 installedChunkData 是否为 0:表示已加载 
  if (installedChunkData === 0) {
    return new Promise(function(resolve) {
      resolve()
    })
  }
  if (installedChunkData) {
    return installedChunkData[2]
  } 
  // 如果 chunk 还未加载,则构造对应的 Promsie 并缓存在 installedChunks 对象中
  var promise = new Promise(function(resolve, reject) {
    installedChunkData = installedChunks[chunkId] = [resolve, reject]
  })
  installedChunkData[2] = promise
  // 构造 script 标签
  var head = document.getElementsByTagName("head")[0]
  var script = document.createElement("script")
  script.type = "text/javascript"
  script.charset = "utf-8"
  script.async = true
  script.timeout = 120000
  if (__webpack_require__.nc) {
    script.setAttribute("nonce", __webpack_require__.nc)
  }
  script.src =
    __webpack_require__.p +
    "static/js/" +
    ({ "0": "alert" }[chunkId] || chunkId) +
    "." +
    { "0": "620d2495" }[chunkId] +
    ".chunk.js"
  var timeout = setTimeout(onScriptComplete, 120000)
  script.onerror = script.onload = onScriptComplete
  function onScriptComplete() {
    script.onerror = script.onload = null
    clearTimeout(timeout)
    var chunk = installedChunks[chunkId]
    // 如果 chunk !== 0 表示加载失败
    if (chunk !== 0) {
        // 返回错误信息
      if (chunk) {
        chunk[1](new Error("Loading chunk " + chunkId + " failed."))
      }
      // 将此 chunk 的加载状态重置为未加载状态
      installedChunks[chunkId] = undefined
    }
  }
  head.appendChild(script)
    // 返回 fullfilled 的 Promise
  return promise
}

结合上面的代码来看,webpack 通过创建 script 标签来实现动态加载的,找出依赖对应的 chunk 信息,然后生成 script 标签来动态加载 chunk,每个 chunk 都有对应的状态:未加载、 加载中、已加载。

我们可以运行 React.lazy 代码来具体看看 network 的变化,为了方便辨认 chunk。我们可以在 import 里面加入 webpackChunckName 的注释,来指定包文件名称。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const OtherComponent = React.lazy(() => import(/* webpackChunkName: "OtherComponent" */'./OtherComponent'));
const OtherComponentTwo = React.lazy(() => import(/* webpackChunkName: "OtherComponentTwo" */'./OtherComponentTwo'));

webpackChunckName 后面跟的就是打包后组件的名称。

图片

打包后的文件中多了动态引入的 OtherComponent、OtherComponentTwo 两个 js 文件。

如果去除动态引入改为一般静态引入:

图片

可以很直观的看到二者文件的数量以及大小的区别。

图片

以上是资源的动态加载过程,当资源加载完成之后,进入到组件的渲染阶段,下面我们再来看看,Suspense 组件是如何接管 lazy 组件的。

Suspense 组件

同样的,先看代码,下面是 Suspense 所依赖的 react-cache 部分简化源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// react/packages/react-cache/src/ReactCache.js 
export function unstable_createResource<I, K: string | number, V>(
  fetch: I => Thenable<V>,
  maybeHashInput?: I => K,
): Resource<I, V> {
  const hashInput: I => K =
    maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
  const resource = {
    read(input: I): V {
      readContext(CacheContext);
      const key = hashInput(input);
      const result: Result<V> = accessResult(resource, fetch, input, key);
      // 状态捕获
      switch (result.status) { 
        case Pending: {
          const suspender = result.value;
          throw suspender;
        }
        case Resolved: {
          const value = result.value;
          return value;
        }
        case Rejected: {
          const error = result.value;
          throw error;
        }
        default:
          // Should be unreachable
          return (undefined: any);
      }
    },
    preload(input: I): void {
      readContext(CacheContext);
      const key = hashInput(input);
      accessResult(resource, fetch, input, key);
    },
  };
  return resource;
}

从上面的源码中看到,Suspense 内部主要通过捕获组件的状态去判断如何加载,上面我们提到 React.lazy 创建的动态加载组件具有 Pending、Resolved、Rejected 三种状态,当这个组件的状态为 Pending 时显示的是 Suspense 中 fallback 的内容,只有状态变为 resolve 后才显示组件。

结合该部分源码,它的流程如下所示:

Error Boundaries 处理资源加载失败场景

如果遇到网络问题或是组件内部错误,页面的动态资源可能会加载失败,为了优雅降级,可以使用 Error Boundaries (https://react.docschina.org/docs/error-boundaries.html) 来解决这个问题。

Error Boundaries 是一种组件,如果你在组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 生命周期函数,它就会成为一个 Error Boundaries 的组件。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI
      return { hasError: true };  
  }
  componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器
      logErrorToMyService(error, errorInfo);
  }
  render() {
    if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染      
        return <h1>对不起,发生异常,请刷新页面重试</h1>;    
    }
    return this.props.children; 
  }
}

你可以在 componentDidCatch 或者 getDerivedStateFromError 中打印错误日志并定义显示错误信息的条件,当捕获到 error 时便可以渲染备用的组件元素,不至于导致页面资源加载失败而出现空白。

它的用法也非常的简单,可以直接当作一个组件去使用,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

我们可以模拟动态加载资源失败的场景。首先在本地启动一个 http-server 服务器,然后去访问打包好的 build 文件,手动修改下打包的子组件包名,让其查找不到子组件包的路径。然后看看页面渲染效果。

图片

可以看到当资源加载失败,页面已经降级为我们在错误边界组件中定义的展示内容。

流程图例:

图片

需要注意的是:错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。

总结

React.lazy() 和 React.Suspense 的提出为现代 React 应用的性能优化和工程化提供了便捷之路。React.lazy 可以让我们像渲染常规组件一样处理动态引入的组件,结合 Suspense 可以更优雅地展现组件懒加载的过渡动画以及处理加载异常的场景。

“注意:React.lazy 和 Suspense 尚不可用于服务器端,如果需要服务端渲染,可遵从官方建议使用 Loadable Components (https://github.com/gregberge/loadable-components)。

参考文档

  1. Concurrent (https://zh-hans.reactjs.org/docs/concurrent-mode-intro.html) 模式
  2. 代码分割 (https://zh-hans.reactjs.org/docs/code-splitting.html)
  3. webpack 优化之code splitting (https://github.com/xiaoxiangdaiyu/webpack_demo/tree/master/codesplitting)
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 政采云技术 微信公众号,前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
深度剖析React懒加载原理
现在前端项目基本都采用打包技术,比如 Webpack,JS逻辑代码打包后会产生一个 bundle.js 文件,而随着我们引用的第三方库越来越多或业务逻辑代码越来越复杂,相应打包好的 bundle.js 文件体积就会越来越大,因为需要先请求加载资源之后,才会渲染页面,这就会严重影响到页面的首屏加载。
xiaofeng123aa
2022/10/04
1.1K0
webpack模块化原理-Code Splitting
webpack的模块化不仅支持commonjs和es module,还能通过code splitting实现模块的动态加载。根据wepack官方文档,实现动态加载的方式有两种:import和require.ensure。
Keller
2021/12/14
9710
React Suspense
前端应用达到一定规模时(比如bundle size以MB为单位),势必面临代码拆分的强需求:
ayqy贾杰
2019/06/12
1.6K0
基于webpack4+react 的js懒加载
此处主要介绍使用动态导入(通过模块中的内联函数调用来分离代码)的懒加载。这种动态代码拆分的方式是webpack提供并推荐选择的方式。其原理是使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。
shirley
2019/03/24
4.5K0
Webpack 打包 commonjs 和 esmodule 动态引入模块的产物对比
接 Webpack 打包 commonjs 和 esmodule 模块的产物对比 我们来继续分析。这篇文章主要来看一下动态引入,允许我们引入的模块名包含变量。
windliang
2022/09/23
9510
Webpack 打包 commonjs 和 esmodule 动态引入模块的产物对比
webpack启动代码源码解读
虽然每天都在用webpack,但一直觉得隔着一层神秘的面纱,对它的工作原理一直似懂非懂。它是如何用原生JS实现模块间的依赖管理的呢?对于按需加载的模块,它是通过什么方式动态获取的?打包完成后那一堆/******/开头的代码是用来干什么的?本文将围绕以上3个问题,对照着源码给出解答。
Dickensl
2022/06/14
7480
webpack启动代码源码解读
【webpack 进阶】Webpack 打包后的代码是怎样的?
webpack 是我们现阶段要掌握的重要的打包工具之一,我们知道 webpack 会递归的构建依赖关系图,其中包含应用程序的每个模块,然后将这些模块打包成一个或者多个 bundle。
coder_koala
2021/03/12
1.4K0
【webpack 进阶】Webpack 打包后的代码是怎样的?
揭开Vue异步组件的神秘面纱
在大型应用里,有些组件可能一开始并不显示,只有在特定条件下才会渲染,那么这种情况下该组件的资源其实不需要一开始就加载,完全可以在需要的时候再去请求,这也可以减少页面首次加载的资源体积,要在Vue中使用异步组件也很简单:
街角小林
2022/03/21
6360
揭开Vue异步组件的神秘面纱
深入了解 webpack 模块加载原理
无论你开发使用的是 CommonJS 规范还是 ES6 模块规范,打包后的文件都统一使用 webpack 自定义的模块规范来管理、加载模块。本文将从一个简单的示例开始,来讲解 webpack 模块加载原理。
谭光志
2020/09/28
1.5K0
5 行代码理解 React Suspense
感谢支持ayqy个人订阅号,每周义务推送1篇(only unique one)原创精品博文,话题包括但不限于前端、Node、Android、数学(WebGL)、语文(课外书读后感)、英语(文档翻译) 如果觉得弱水三千,一瓢太少,可以去 http://blog.ayqy.net 看个痛快
ayqy贾杰
2019/12/16
1.5K0
Webpack原理-输出文件分析
虽然在前面的章节中你学会了如何使用 Webpack ,也大致知道其工作原理,可是你想过 Webpack 输出的 bundle.js 是什么样子的吗? 为什么原来一个个的模块文件被合并成了一个单独的文件?为什么 bundle.js 能直接运行在浏览器中? 本节将解释清楚以上问题。
IMWeb前端团队
2019/12/03
6990
Webpack原理-输出文件分析
React lazy 原来是这么动态加载组件的!
最近没有开发任务,除了研究下安排的优化代码的任务,顺便看了下React.lazy源码!所以今天主要介绍 React.lazy 的实现原理!
萌萌哒草头将军
2025/05/19
1510
React lazy 原来是这么动态加载组件的!
【模块化】:Webpack 是如何将不同规范(ESM、CJS、UMD、AMD、CMD)的模块化代码打包到一起并协调它们运行的?
在一个项目中同时使用 ES6、CJS、CMD、AMD、UMD 5种不同的模块化规范编写代码,并同时应用静态导入、动态导入(Dynamic Import)方法来引用这些模块。观察 Webpack 是如何将这些不同模块化规范的代码打包到一起和协调它们运行的。
WEBJ2EE
2022/03/30
7.3K2
【模块化】:Webpack 是如何将不同规范(ESM、CJS、UMD、AMD、CMD)的模块化代码打包到一起并协调它们运行的?
React 进阶 - 渲染调优
Suspense 是 React 提出的一种同步的代码来实现异步操作的方案。Suspense 让组件‘等待’异步操作,异步请求结束后在进行组件的渲染,即异步渲染。
Cellinlab
2023/05/17
1K0
React 进阶 - 渲染调优
React Suspense 尝鲜,处理前后端IO异步操作
Suspense主要用来解决网络IO问题,它早在2018年的React 16.6.0版本中就已发布。它的相关用法有些已经比较成熟,有的相对不太稳定,甚至经历了重命名、删除:
葡萄城控件
2022/06/19
9000
React Suspense 尝鲜,处理前后端IO异步操作
React 的 Suspense 和 ErrorBoundary 还有这种关系?
Suspense 组件想必大家都用过,一般是和 React.lazy 结合用,用来加载一些异步组件。
神说要有光zxg
2024/01/02
3500
React 的 Suspense 和 ErrorBoundary 还有这种关系?
使用React.Suspense显示loading效果
它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。
Qiang
2019/08/15
2.5K0
[译] 延迟加载 React Components (用 react.lazy 和 suspense)
虽然在 React 16.8.1 中终于面世的 hooks 引人瞩目,但在去年发布的 16.6.0 版本里也包含了一个吸引人的新特性,可以让我们在不依赖第三方库的情况下简化对延迟加载(lazy loading)的处理。
江米小枣
2020/06/15
3.3K0
「React进阶」深度剖析 React 异步组件前世与今生
今天我们聊一聊React中的异步组件的现况和未来,异步组件很可能是未来从数据交互到UI展示一种流畅的技术方案,所以既然要吃透React,进阶React,就有必要搞懂异步组件。
用户3806669
2021/07/06
1.8K0
「React进阶」深度剖析 React 异步组件前世与今生
深度理解 React Suspense(附源码解析)
本文介绍与 Suspense 在三种情景下使用方法,并结合源码进行相应解析。欢迎关注个人博客。
牧云云
2019/03/15
1.5K0
相关推荐
深度剖析React懒加载原理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验