QCon 全球软件开发大会(北京站)门票 9 折倒计时 4 天,点击立减 ¥880 了解详情
写点什么

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

2021 年 3 月 22 日

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

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


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


// 不使用 React.lazyimport OtherComponent from './OtherComponent';// 使用 React.lazyconst OtherComponent = React.lazy(() => import('./OtherComponent'))
复制代码


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



// 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 的内容,否则不展示。


 <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 检测到这种语法会自动代码分割。使用这种动态导入语法代替以前的静态引入,可以让组件在渲染的时候,再去加载组件对应的资源,这个异步加载流程的实现机制是怎么样呢?


话不多说,直接看代码:

__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 的注释,来指定包文件名称。


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 部分简化源码:


// 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 的组件。


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 时便可以渲染备用的组件元素,不至于导致页面资源加载失败而出现空白。

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


<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)。



头图:Unsplash

作者:大柱

原文:https://mp.weixin.qq.com/s/l_kv6rzUXSF3R9bfIko5BQ

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

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021 年 3 月 22 日 23:401797

评论

发布
暂无评论
发现更多内容

作业-05-java实现一致性hash算法

梦子说

极客大学架构师训练营

Java实现一致性 Hash 算法实现(训练营第五课)

看山是山

极客大学架构师训练营 一致性hash

golang实现基于虚拟节点的一致性hash算法

朱月俊

第五次作业

王锟

真懂Spring的@Configuration配置类?你可能自我感觉太良好

YourBatman

Spring Boot Spring Framework @Configuration Spring配置类

第五周总结-缓存、消息中间件、负载均衡器、分布式数据库

吴建中

极客大学架构师训练营

扯淡 Java 集合

CoderLi

Java 后端 hashmap 后台

架构师训练营」第 4 周作业

edd

分布式缓存总结

朱月俊

领域模型为核心的架构设计 初篇

小隐乐乐

领域驱动设计 架构师

架构师训练营 第五周 学习总结

一雄

学习 极客大学架构师训练营 第五周

一致性哈希算法实现及案例测试,java版

潜默闻雨

视读——沟通的艺术,看入人里,看出人外(第一章)

废材姑娘

读书笔记 视觉笔记

可变对象和不可变对象

Leetao

Python Python基础知识

极客时间架构师训练营 - week5 - 作业 2

jjn0703

极客大学架构师训练营

架构师训练营第五周作业 设计分布式缓存系统

Melo

极客大学架构师训练营

一致性hash算法及java实现(转载,学习了)

王锟

架构师课程第五周总结

dongge

技术选型之缓存、队列、负载均衡

olderwei

极客大学架构师训练营

架构师训练营 Week 05 总结

Wancho

架构师训练营:第五周作业-一致性 hash实现

zcj

极客大学架构师训练营

嗯?阿里为啥不用 ZooKeeper 做服务发现?

Java小咖秀

zookeeper 分布式 技术人生

第5周作业

田振宇

高性能系统设计

dapaul

极客大学架构师训练营

架构师训练营-第五周-命题作业

sljoai

极客大学架构师训练营 第五周

「架构师训练营」学习笔记:第 5 周 技术选型

Amy

总结 极客大学架构师训练营 消息队列 分布式缓存 第五周

第 5 周 - 课后作业

大海

实现一致性哈希算法

Aldaron

【第九课 + 第十课】技术选型:缓存架构 + 消息队列与异步架构

Aldaron

架构师训练营作业 -- Week 5

吴炳华

极客大学架构师训练营

重学 Java 设计模式:实战模版模式「模拟爬虫各类电商商品,生成营销推广海报场景」

小傅哥

Java 设计模式 小傅哥 重构 代码规范

边缘计算隔离技术的挑战与实践

边缘计算隔离技术的挑战与实践

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