Lemonreds / snippets

code snippets.
https://github.com/Lemonreds/snippets/issues
2 stars 0 forks source link

[2020-09-25]: Webpack 代码分割 动态导入的实践例子 #17

Open Lemonreds opened 4 years ago

Lemonreds commented 4 years ago

代码分割,动态导入的场景

简单来说,就是例如我们的页面中,有一个组件内部实现逻辑非常庞大,引入了 许多其他的包,导致该页面整体体积过大,影响首屏渲染速度,此时我们期望优先加载 其他内容,最后再去加载这块组件内容,避免首屏渲染时间过长。也就是从chunks再拆分出 部分逻辑,单独生成一个script文件,在我们需要的时机进行请求该模块的script文件,再将 它显示出来,这样就是代码分割,动态引入的一个经典场景。

动态导入的实现原理

动态导入,本质上是来自于ECMA关于异步import语法的提案,该语法是让import支持异步引入。 例如import('./xxxfile').then(res=>{}),使得import语法promise异步化,res中就可以拿到 该模块的语法资源,再由开发者对其进行处理,例如,在webpack的实现中,引入一个react组件的res是这样的 一个JS对象: image

其中default便是该模块默认到处的内容,也就是我们默认导出的组件了。

实践例子

1.模拟一下场景,我们有一个需要被拆分的 react 组件 DynamicTest.js

import React from 'react';
import styles from './DynamicTest.less';

function DynamicTest() {
  // 实际案例中,这里的实现逻辑可能非常复杂,我们想把它单独拎出来
  return <div className={styles.root}>动态加载组件内容</div>;
}

export default DynamicTest;

以及其组件样式文件 ./DynamicTest.less, 现在我们期望把这个文件和其样式文件单独从打包chunk中拆分出来。

原来的webpack输出:

chunk.js      
chunk.css

期望的webpack输出:

chunk.js
chunk.css          
DynamicTest.js               
DynamicTest.chunk.css        

2.对动态导入的逻辑进行封装

在对DynamicTest进行动态导入的时候,我们期望能实现以下功能 createLoadable:

也就是以下的调用例子,createLoadable传入以上需求的配置,返回一个可动态导入的react组件:

const DynamicTest = createLoadable({
  // loader(func),返回动态导入的文件路径
  // 其中 /* webpackChunkName: "DynamicTest" */ 是webpack规定的,来命名打包后文件的名字
  loader: () => import(/* webpackChunkName: "DynamicTest" */ './DynamicTest'),
  // loading(reactComponent) react组件,props是loading(正在加载)以及err(发生错误)
  // 我们可以根据其props进行样式定制化
  loading: ({loading,err})=> loading ? 'loading' : err ? 'error': null,
  // delay(ms) 延迟加载的时间
  delay: 5000,
});

在我们的页面中,调用DynamicTest组件

export default () => { 
  return (
    <Wrapper>
      <DynamicTest/>
    </Wrapper>
  );
};
  1. createLoadable的实现逻辑
import React, { useState, useEffect } from 'react';

// loader组件,控制动态引入的状态
function Loader({ loader, loading, delay, loadedComponentProps }) {
  // import进来的模块内容
  const [loaded, setLoaded] = useState(null);
  // err引入发生的错误,isLoading是否正在加载中
  const [err, setErr] = useState(null);
  const [isLoading, setLoading] = useState(false);

  const load = () => {
    loader()
      .then(_loaded => {
        // log =>{__esModule: true, Symbol(Symbol.toStringTag): "Module", default: ƒ}
        setLoaded(_loaded);
      })
      .catch(_err => {
        setErr(_err);
      })
      .finally(() => {
        setLoading(false);
      });
  };
  useEffect(() => {
    setLoading(true);
    let h = null;
    if (delay) {
      h = setTimeout(load, delay);
    } else {
      load();
    }
    return () => {
      clearTimeout(h);
    };
  }, []);

  // 加载中或者错误
  // 返回占位符组件
  if (isLoading || err) {
    return React.createElement(loading, { isLoading, err });
  }

  // 加载成功
  // 返回加载成功后的组件
  if (loaded) {
    const loadedComponent =
      loaded && loaded.__esModule ? loaded.default : loaded;

    return React.createElement(loadedComponent, loadedComponentProps);
  }

  return null;
}

// 默认的占位符组件
const DefaultLoading = ({ loading, error }) => {
  // 组件正在异步加载的时候
  if (loading) return <div>Loading</div>;
  // 组件加载发生了错误的时候
  if (error) return <div>Unknown error occurred</div>;
  return null;
};

/**
 * @method createLoadable
 * @desc 通过import动态导入的语法,返回一个React组件,在合适的时机展示loading与err以及异步加载组件的内容
 * @param {loadr|fn}  () => import('./DynamicTest') 动态引入
 * @param {loading|component}  占位符组件,会被注入isLoading、err的props
 * @param {delay|ms}  延迟加载的时间
 * */
const createLoadable = ({ loader, loading = DefaultLoading, delay = 0 }) => {
  return props =>
    React.createElement(Loader, {
      loader,
      loading,
      delay,
      loadedComponentProps: props,
    });
};

export default createLoadable;

createLoadable的实现简而言之,就是创建了一个react组件loader,该组件帮助我们来管理import动态导入时的状态, 加载中、加载失败、以及加载完成,再去选择展示的组件,以及加上了延迟加载的功能需要。

预览

  1. 正在加载的时候 image
  2. 加载完成,展示组件内容 image
  3. network中单独出来的chunk image

其他:如何修改打包后的分片文件名

1.使用webpack,修改

output: {
 chunkFilename: '[name].bundle.js',
}

2.使用webpack-chain

config.output.chunkFilename('[name].bundle.js');

output中的chunkFilename其实就是entry中没有的其他chunk的命名规则。

总结

以上便是在react中如何使用webpack的动态导入特性的一个实践例子了,其实就是利用了webpack对import异步化语法的支持, 文件拆分的逻辑由webpack完成,我们通过编写一个组件来管理动态引入可能出现的几种状态,来显示我们期望的内容,甚至还可以 在导入失败的时候重新加载。

参考

webpack关于动态导入的的文档: https://webpack.js.org/guides/code-splitting/#dynamic-imports

以上需求的一个更好的实现,UMIJS中动态导入的功能也基于此项目 https://github.com/jamiebuilds/react-loadable

Lemonreds commented 4 years ago

https://github.com/Lemonreds/react-components/tree/master/src/pages/comp/examples/createLoadable 源码例子