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

50 篇原创好文~
本文首发于政采云前端团队博客: 这就是你日思夜想的 React 原生动态加载

React.lazy 是什么

随着前端应用体积的扩大,资源加载的优化是咱们必需要面对的问题,动态代码加载就是其中的一个方案,webpack 提供了符合 ECMAScript 提案 的 import() 语法 ,让咱们来实现动态地加载模块(注:require.ensure 与 import() 均为 webpack 提供的代码动态加载方案,在 webpack 2.x 中,require.ensure 已被 import 取代)。javascript

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

使用 React.lazy

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

// 不使用 React.lazy
import OtherComponent from './OtherComponent';
// 使用 React.lazy
const OtherComponent = React.lazy(() => import('./OtherComponent'))

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

图片

// 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

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

图片

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

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

接下来咱们能够看看渲染效果,为了更清晰的展现加载效果,咱们将网络环境设置为 Slow 3G。web

图片

组件的加载效果:api

图片

能够看到在组件未加载完成前,展现的是咱们所设置的 fallback 组件。

在动态加载的组件资源比较小的状况下,会出现 fallback 组件一闪而过的的体验问题,若是不须要使用能够将 fallback 设置为 null。

固然针对这种场景,React 也提供了对应的解决方案,在 Concurrent Mode 模式下,给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 来解决这个问题。

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

参考文档

  1. Concurrent 模式
  2. 代码分割
  3. webpack 优化之code splitting

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com

相关文章
相关标签/搜索