React Loadable 介绍

Ba la la la ~ 读者朋友们,你们好啊,又到了冷锋时间,话不多说,发车!


React 组件代码分割和加载

当你的应用足够庞大时,把所有代码简单地打成一个 bundle,启动时间会很长。你需要将 app 分割成几个 bundle,按需加载。


A single giant bundle vs. multiple smaller bundles

Browserify 和Webpack 等工具可以很好地解决如何将一个大 bundle 分割的问题。

那么你就需要决定在哪儿可以分离出另一个 bundle 进行异步加载。App 还需要在加载时给用户提示。

基于路由的分割 vs 基于组件的分割

通常的建议是将 app 分成独立的路径,然后每个异步加载。这对大多 app 都适用,点击链接然后加载一个新的页面,这种体验还可以。

但是我们可以做得更好。

React 的多数路由工具都是一个路径就是一个组件。没什么特别的。如果我们在组件上进行优化而不是让路径来负责这个任务会怎样呢?


Route vs. component centric code splitting

显然组件的方式更好些。你可以轻松地在更多地方分割 app,Modals、tabs以及很多用户触发才展示内容的 UI 组件等,而不仅是路径。

更不用说那些延迟加载直到高优先级的内容加载完的地方。页面底部的组件加载一堆库:为什么在顶部时就要加载那些库呢?

你也可以简单地按路由分割,因为它们也是组件。看哪种方式更适合你的 app 了。

但是我们需要让组件级分割和路由一样简单。新的分割应该改几行代码就可以了,其它都会自动完成。

React Loadable 介绍

大家都说组件分割很难实现,然后我就写了一个小库——React Loadable。

Loadable 是一款可以轻松分割组件级 bundle 的高阶组件(创建组件的函数)。

假设有两个组件,其中一个引入并渲染另一个。

import AnotherComponent from './another-component';

class MyComponent extends React.Component {
  render() {
    return <AnotherComponent/>;
  }
}

目前通过 import 同步引入 AnotherComponent 这个依赖。我们需要一种可以异步加载的方式。

dynamic import(目前处于第 3 阶段的 tc39 提议)可以使组件异步加载 AnotherComponent。

class MyComponent extends React.Component {
  state = {
    AnotherComponent: null
  };

  componentWillMount() {
    import('./another-component').then(AnotherComponent => {
    this.setState({ AnotherComponent });
  });
}

  render() {
    let {AnotherComponent} = this.state;
     if (!AnotherComponent) {
       return <div>Loading...</div>;
     } else {
      return <AnotherComponent/>;
    };
  }
}

但是这需要一系列的人为操作,而且有许多不同的场景无法适用。import() 失败了怎么办呢?服务端渲染呢?

这个问题可以用 Loadable 进行抽象。Loadable 使用起来很简单,只要传入加载组件的函数和加载组件过程中展示的“Loading”组件就可以了。

import Loadable from 'react-loadable';

function MyLoadingComponent() {
  return <div>Loading...</div>;
}

const LoadableAnotherComponent = Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent
});

class MyComponent extends React.Component {
  render() {
    return <LoadableAnotherComponent/>;
  }
}

但是如果组件加载失败了呢?我们还需要有 error 状态。

为了给你最大的控制权,决定什么时候展示什么,error 只会简单地作为 LoadingComponent 的属性抛出。

function MyLoadingComponent({ error }) {
  if (error) {
    return <div>Error!</div>;
  } else {
    return <div>Loading...</div>;
  }
}

import() 自动分割代码

import() 的一个优点是增加新代码时,Webpack 2 可以自动分割代码。

也就是说你只要使用 React Loadable、改用 import(),就可以轻松地用新的代码分割点进行试验,来看看哪种方法最适合你的应用。

此处可以查看示例项目,或者查阅 Webpack 2 文档(注:一些相关文档位于 require.ensure() 章节)。

Loading 组件避免一闪而过

有时组件加载很快(<200ms),loading 屏只在屏幕上一闪而过。

一些用户研究已证实这会导致用户花更长的时间接受内容。如果不展示任何 loading 内容,用户会接受得更快。

所以 loading 组件有一个 pastDelay 属性,仅在组件加载时间超过设置的 delay 时值为 true。

export default function MyLoadingComponent({ error, pastDelay }) {
  if (error) {
    return <div>Error!</div>;
  } else if (pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

delay 默认是 200ms,可以向 Loadable 传递第 3 个参数自定义 delay。

Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent,
  delay: 300
});

预加载

你也可以在组件渲染前预加载进行优化。

例如需要在点击按钮时加载新的组件,就可以在用户悬浮在按钮上时预加载组件。

Loadable 创建的组件会暴露一个 preload 静态方法用来实现上述效果。

let LoadableMyComponent = Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent,
});

class MyComponent extends React.Component {
  state = { showComponent: false };

  onClick = () => {
    this.setState({ showComponent: true });
  };

  onMouseOver = () => {
    LoadableMyComponent.preload();
  };
 
  render() {
    return (
      <div>
        <button onClick={this.onClick} onMouseOver={this.onMouseOver}>
          Show loadable component
        </button>
        {this.state.showComponent && <LoadableMyComponent/>}
      </div>
    )
  }
}

服务端渲染

Loader 通过最后一个参数支持服务端渲染。

向正在异步加载的模块传递精确路径,Loader 就会在服务端运行时同步地 require() 模块。

import path from 'path';

const LoadableAnotherComponent = Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent,
  delay: 200,
  serverSideRequirePath: path.join(__dirname, './another-component')
});

也就是说经过异步加载、代码分割的 bundle 可以在服务端同步渲染。这样客户端获取备份会有问题。我们可以在服务端渲染全部应用,但是在客户端需要一个个加载 bundle。

但是如果我们可以指定哪些 bundle 需要加入服务端的 bundle 进程呢?那么我们就可以一次性向客户端装载那些 bundle了,客户端就可以准确获取服务端渲染的状态了。

在 Loadable 中我们能够拿到服务端需要的全部路径,所以我们可以增加一个 flushServerSideRequires 函数,返回最后在服务端渲染的所有路径。然后通过 webpack --json 命令可以匹配文件和该文件结束所在的 bundle(此处查看代码)。


以上为个人意见,如有雷同,纯属巧合,欢迎大家多提意见!Bey 了 个 Bey ~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容