# React 优化_ReactLoadable

一个动态导入加载组件的高阶组件,实现 code-splitting(代码分割)

# 什么是代码分割

就是把项目中一个大的入口文件分割成多个小的,单独的文件进程。 ep:

import Loadable from "react-loadable";
import Loading from "./my-loading-component";

const LoadableComponent = Loadable({
  loader: () => import("./my-component"),
  loading: Loading,
});
export default class App extends React.Component {
  render() {
    return <LoadableComponent />;
  }
}

# Route-based splitting(基于路由的代码分割) vs. Component-based splitting(基于组件的代码分割)

在大多数应用中,一个路由往往会包含多个组件,像 Modal, tabs 等 UI 组件,而用户并不一定是会去操作这些,所以基于组件分割,当用户操作或需要时在加载对应的组件会大大节约流量,太高访问速度。

# 前后对比

未使用

//
import Bar from "./components/Bar";
class foo extends React.Component {
  render() {
    return <Bar />;
  }
}

和 foo 同步渲染,但在 app 渲染之前,可以先渲染重要的,Bar 延后渲染,所以我们需要的是

import loadable from "react-loadable";
class MyComponent extends React.Component {
  state = {
    Bar: null,
  };
  componentWillMount() {
    import("./components/Bar").then((Bar) => {
      this.setState({ Bar });
    });
  }
  render() {
    let { Bar } = this.state;
    if (!Bar) {
      return <Loading />;
    } else {
      return <Bar />;
    }
  }
}

以上代码的复杂度提升了很多,还有 import 失败的话怎么办,服务端渲染怎么办等等

// 用loadable改装
import Loadable from "react-loadable";
const LoadableBar = Loadable({
  loader: () => import("./components/Bar"),
  loading() {
    return <Loading />;
  },
});

# loading 组件优化

function Loading(props) {
  if (props.error) {
    return <div>Error!</div>;
  } else if (props.timedOut) {
    return <div>Taking a long time...</div>;
  } else if (props.pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}
// 用单独的loading组件
Loadable({
  loader: () => import("./components/Bar"),
  loading: Loading,
  delay: 300, // .3s
  timeout: 10000, // 10s
});
// 自定义渲染 Loadable里的render
Loadable({
  loader: () => import("./myComponent"), // 这样在myComponent里使用 loading而来控制UI
  render(loaded, props) {
    let Component = loaded.namedExport;
    return <Component {...props} />;
  },
});

# 加载更多资源 Loadable.Map

Loadable.Map({
  loader: {
    Bar: () => import("./Bar"),
    i18n: () => fetch("./i18n/bar.json").then((res) => res.json()),
  },
  render(loaded, props) {
    let Bar = loaded.Bar.default;
    let i18n = loaded.i18n;
    return <Bar {...props} i18n={i18n} />;
  },
});

# 预加载

可以决定哪些组件在渲染之前进行预先加载,具体用法如下

const LoadableBar = Loadable({
  loader: ()=> import('./Bar');
  loading: Loading,
})
class MyComponent extends React.Component {
  state = {showBar: false};
  onClick = ()=>{
    this.setState({showBar: true});
  };
  onMouseOver = ()=>{
    LoadableBar.preload(); // 预加载
  };
  render(){
    return (
      <div>
        <button
          onClick={this.onClick}
          onMouseOver={this.onMouseOver}
        >
          Show Bar
        </button>
        {this.state.showBar && <LoadableBar />}
      </div>
    )
  }
}

# 服务端渲染

import express from "express";
import React from "react";
import ReactDOMServer from "react-dom/server";
import App from "./components/app";
const app = express();
app.get("/", (req, res) => {
  res.send(`
    <!doctype html>
    <html lang='en'>
      <head>...</head>
      <body>
        <div id="app">${ReactDOMServer.renderToString(<App />)}</div>
        <script src="/dist/main.js">
      </body>
    </html>
  `);
});

# Loadable.preloadAll预加载所有组件

Loadable.preloadAll().then(() => {
  app.listen(3000, () => {
    conosl;
  });
});

# 声明哪个模块被加载

babel.config.js中加react-loadable/babel

{
  "plugins": [
    "react-loadable/babel"
  ]
}

# 找出哪些动态模块正在被加载,将加载的模块映射到打包文件上,客户端会等待所有打包文件加载完成

1,用Loadable.Capture收集所有被加载的模块

import Loadable from "react-loadable";
app.get("/", (req, res) => {
  let modules = [];
  let html = ReactDOMServer.renderToString(
    <Loadable.Capture report={(moduleName) => modules.push(moduleName)}>
      <App />
    </Loadable.Capture>
  );
  console.log(modules);
  res.send(`...${html}...`);
});

2, 将加载的模块映射到打包文件上

import { ReactLoadablePlugin } from "react-loadable/webpack";
export default {
  plugins: [
    new ReactLoadablePlugin({
      filename: "./dist/react-loadable.json",
    }),
  ],
};

3, 将模块转换为打包文件,并输入到 html 中

import express from 'express';
import React from 'react';
import Loadable from "react-loadable";
import {getBundles} from "react-loadable/webpack";
import stats from "./dist/react-loadable.json";
import App from './components/app';

const app = express();
app.get("/", (req, res)=>{
  let modules = [];
  let html = ReactDOMServer.renderToString(
    <Loadable.Capture report={moduleName=> modules.push(moduleName)}>
      <App />
    </Loadable.Capture>
    let bundles = getBundles(stats, modules);
    res.send(`
      <!doctype html>
      <html lang="en">
        <head>...</head>
        <body>
          <div id="app">${html}</div>
          <script src="dist/main.js"></script>
          ${bundles.map(bundle=>{
            return `<script src="/dist/${bundle.file}"></script>`
          }).join(`\n`)}
        </body>
      </html>
    `)
  )
})

4, 由于 Webpack 工作方式是:主打包文件会比其他的 scripts 预先加载,但我们需要等待所有文件加载后才开始渲染

// 客户端
import React from "react";
import ReactDOM from "react-dom";
import Loadable from "react-loadable";
import App from "./components/App";
window.main = () => {
  Loadable.preloadReady().then(() => {
    // hydrate 因为ssr时,服务器输出的是字符串,而浏览器端需要根据这些字符串完成react的初始化工作,比如创建组件实例,这样才能响应用户操作,这个过程叫做hydrate,或re-hydrate,即给干瘪的字符串注水
    // hydrate 描述的是 ReactDOM复用ReactDOMServer服务端渲染的内容时尽可能保留结构,并补充事件绑定等Client特有的内容过程
    ReactDOM.hydrate(<App />, document.getElementById("app"));
  });
};
// server
....
let bundles = getBundles(stats, modules);
res.send(`
  ...
  <script src="/dist/main.js"></script>
  ${bundles.map(...).join("\,")}
  <script>window.main;</scripts>
`);

# Loadable 组件

import React from "react";
import Loadable from "react-loadable";
import { ActivityIndicator } from "antd-mobile";
import { Scoped } from "kremling";
import styles from "./index.krem.scss";

const Loading = ({ isLoading, error }) => {
  return (
    <Scoped css={styles}>
      <div className="react-loadable">
        {isLoading ? (
          <ActivityIndicator text="组件加载中..." />
        ) : error ? (
          `组件加载错误:${error}`
        ) : null}
      </div>
    </Scoped>
  );
};

export default (loader) =>
  Loadable({
    loader,
    loading: Loading,
  });

// 使用
import Loadable from "@/components/loadable";
const myCompant = Loadable(() => import("@/pages/myCompant"));
React.render(myCompant);