基于webpack Code Splitting实现react组件的按需加载

简介: 随着web应用功能越来越复杂,模块打包后体积越来越大,如何实现静态资源的按需加载,最大程度的减小首页加载模块体积和首屏加载时间,成为模块打包工具的必备核心技能。 webpack作为当下最为流行的模块打包工具,成为了react、vue等众多热门框架的官方推荐打包工具。其提供的Code Splitting(代码分割)特性正是实现模块按需加载的关键方式。 # 什么是Code Splitt

随着web应用功能越来越复杂,模块打包后体积越来越大,如何实现静态资源的按需加载,最大程度的减小首页加载模块体积和首屏加载时间,成为模块打包工具的必备核心技能。

webpack作为当下最为流行的模块打包工具,成为了react、vue等众多热门框架的官方推荐打包工具。其提供的Code Splitting(代码分割)特性正是实现模块按需加载的关键方式。

什么是Code Splitting

官方定义:

Code splitting is one of the most compelling features of webpack. It allows you to split your code into various bundles which you can then load on demand — like when a user navigates to a matching route, or on an event from the user. This allows for smaller bundles, and allows you to control resource load prioritization, which if used correctly, can have a major impact on your application load time

翻译过来大概意思就是:

代码分割功能允许将web应用代码分割为多个独立模块,当用户导航到一个匹配的路由或者派发某些特定事件时,来按需加载这些模块。这样就可以将大型的模块分割为多个小型的模块,实现系统资源加载的最大优化,如果使用得当,能够极大的减少我们的应用首屏加载时间。

Code splitting 分类

一、缓存和并行加载的资源分割

这种方法是将某些第三方基础框架模块(例如:moment、loadash)或者多个页面的公用模块拆分出来独立打包加载,通常这些模块改动频率很低,将其与业务功能模块拆分出来并行加载,一方面可以最大限度的利用浏览器缓存,另一方面也可以大大降低多页面系统的代码冗余度。

按照资源类型不同又可以分为js公共资源分割和css资源分割两类。

1、js公共资源分割

例如:系统应用入口文件index.js中使用日期功能库 momentjs。

index.js

var moment = require('moment');
console.log(moment().format());

webpack.config.js

定义多个entry入口

  • main为主入口模块文件
  • vendor为公共基础库模块,名字可随意设定。称为initial chunk

var path = require('path');

module.exports = {
   entry: {
       main: './index.js',
       vendor: ['moment']
   },
   output: {
       filename: '[name].js',
       path: path.resolve(__dirname, 'dist')
   }
}

执行webpack打包命令:

webpack --progress --hide-modules

可以看到最终打包为两个js文件 main.js 、vendor.js,但如果检查者两个文件会发现moment模块代码被重复打包到两个文件中,而这肯定不是我们想要的,这时候就需要 webpack的plugin发挥作用了。

vendo

使用CommonsChunkPlugin

var webpack = require('webpack');
var path = require('path');

module.exports = {
   entry: {
       main: './index.js',
       vendor: ['moment']
   },
   output: {
       filename: '[chunkhash:8].[name].js',
       path: path.resolve(__dirname, 'dist')
   },
   plugins: [
       new webpack.optimize.CommonsChunkPlugin({
            // vendor是包括公共的第三方代码,称为initial chunk
           name: 'vendor'
       })
   ]
}

执行webpack打包命令,我们发现moment只被打包进vendor.js中。

vendo

webpack运行时模块(manifest)

  • 在前面的步骤2当中webpack在浏览器中加载js模块的运行时代码块也打包进了vendor.js,如果为打包的js文件添加chunkhash,则每次修改 index.js后再次编译打包,由于运行时代码需要重新编译生成,导致vendor.js重新打包并生成新的chunkhash。

webpack运行时代码块部分:

webpackjsonp

  • 实际项目中我们希望修改业务功能后打包时只重新打包业务模块,而不打包第三方公共基础库。这里我们可以将webpack的运行时代码提取到独立的manifest文件中,这样每次修改业务代码只重新打包生成业务代码模块main.js和运行时代码模块manifest.js,就实现了业务模块和公共基础库模块的分离。

manifest1

  • names字段支持以数组格式来指定基础库模块名称和运行时代码模块名称。
module.exports = {
   entry: {
       main: './index.js',
       vendor: 'moment'
   },
   output: {
       filename: '[chunkhash:8].[name].js',
       path: path.resolve(__dirname, 'dist')
   },
   plugins: [
       new webpack.optimize.CommonsChunkPlugin({
           // manifest是包括webpack运行时runtime的块,可以称为entry chunk
           names: ['vendor', 'manifest']
       })
   ]
}

2、CSS代码分割

  • 实际项目开发当中经常使用webpack的css-loader来将css样式导入到js模块中,再使用style-loader将css样式以<style>标签的形式插入到页面当中,但这种方法的缺点就是无法单独加载并缓存css样式文件,页面展现必须依赖于包含css样式的js模块,从而造成页面闪烁的不佳体验。
  • 因此有必要将js模块当中import的css模块提取出来,这时候就需要用到extract-text-webpack-plugin。

注意webpack2.x需要使用相应版本的plugin。

npm i --save-dev extract-text-webpack-plugin@beta 

index.js

import moment from 'moment';
import './index.css';

console.log('moment:', moment().format());

webpack.config.js

var ExtractTextPlugin = require('extract-text-webpack-plugin');
......
   module: {
       rules: [{
           test: /\.css$/,
           exclude: /node_modules/,
           use: ExtractTextPlugin.extract({
               loader: 'css-loader',
               options: {}
           })
       }]
   },
   plugins: [
       new ExtractTextPlugin({ 
           filename: 'bundle.css', 
           disable: false, 
           allChunks: true 
       })
   ]
......

extract

二、按需加载代码分割

  • 前面介绍的静态资源分离打包需要开发者在webpack配置文件中明确分割点来提取独立的公共模块,这种方式适合提取第三方公共基础库(vue、react、moment等)以及webpack 的运行时代码模块。
  • 除此之外webpack还提供了按需加载的代码分割功能,常用于在web应用路由或者用户行为事件逻辑中动态按需加载特定的功能模块chunk,这就是我们本文中后面要重点介绍的。

Code splitting with require.ensure

webpack1提供了CommonJS风格的 require.ensure()实现模块chunk的异步加载,通过require.ensure()在js代码中建立分割点,编译打包时webpack会将此分割点所指定的代码模块都打包为一个代码模块chunk,然后通过jsonp的方式来按需加载打包后的模块chunk。

  • require.ensure()语法
// 空参数
require.ensure([], function(require){
    var = require('module-b');
});

// 依赖模块 "module-a", "module-b",会和'module-c'打包成一个chunk来加载
// 不同的分割点可以使用同一个chunkname,这样可以保证不同分割点的代码模块打包为一个chunk
require.ensure(["module-a", "module-b"], function(require) {
    var a = require("module-a");
    var b = require("module-b");
    var c = require('module-c');
},"custom-chunk-name");

Code Splitting with ES2015

webpack2 的ES2015 loader中提供了import()方法在运行时动态按需加载ES2015 Module。

webpack将import()看做一个分割点并将其请求的module打包为一个独立的chunk。import()以模块名称作为参数名并且返回一个Promise对象。

  • import() 语法
import("./module").then(module => {
    return module.default;
}).catch(err => {
    console.log("Chunk loading failed");
});
  • import()使用须知

    • import()目前还是处于TC39 proposal阶段。
    • 在Babel中使用import()方法,需要安装 dynamic-import插件并选择使用babel-preset-stage-3处理解析错误。
  • 动态表达式 Dynamic expressions

    import()中的传参可支持部分表达式的写法了,如果之前有接触过CommonJS中require()表达式写法,应该不会对此感到陌生。

    它的操作其实和 CommonJS 类似,给所有可能的文件创建一个环境,当你传递那部分代码的模块还不确定的时候,webpack 会自动生成所有可能的模块,然后根据需求加载。这个特性在前端路由的时候很有用,可以实现按需加载资源。

    import() 会针对每一个读取到的module创建独立的chunk。

    function route(path, query) {
      return import(`./routes/${path}/route`)
        .then(route => new route.Route(query));
    }

bundle-loader

bundle-loader 是webpack官方提供的loader,其作用就是对require.ensure的抽象封装为一个wrapper函数来动态加载模块代码,从而避免require.ensure将分割点所有模块代码打包为一个chunk体积过大的问题。

  • 使用语法:
// 在require bundle时,浏览器会立即加载
var waitForChunk = require("bundle!./file.js");
 
// 使用lazy模式,浏览器并不立即加载,只在调用wrapper函数才加载
var waitForChunk = require("bundle?lazy!./file.js");
 
// 等待加载,在回调中使用
waitForChunk(function(file) {
    var file = require("./file.js");
});
  • wrapper函数:

默认普通模式wrapper:


var cbs = [],data;
module.exports = function(cb) {
    if(cbs) cbs.push(cb);
    else cb(data);
},
require.ensure([], function(require) {
    data = require('./file.js');
    var callbacks = cbs;
    cbs = null;
    for(var i = 0, l = callbacks.length; i < l; i++) {
        callbacks[i](data);
      }
});

lazy模式wrapper:

module.exports = function (cb) {
  require.ensure([], function(require) {
    var app = require('./file.js');
    cb(app);
  });
};

使用bundle-loader在代码中require文件的时候只是引入了wrapper函数,而且因为每个文件都会产生一个分离点,导致产生了多个打包文件,而打包文件的载入只有在条件命中的情况下才产生,也就可以按需加载。

  • 支持自定义Chunk名称:
require("bundle-loader?lazy&name=my-chunk!./file.js");

promise-loader

promise-loader是bundle-loader的lazy模式的变种,其核心就是使用Promise语法来替代原先的callback回调机制。与bundle-loader类似,require模块的时候只是引入了wrapper函数,不同之处在于调用函数时得到的是一个对模块引用的promise对象,需要在then方法中获取模块对象,并可以使用catch方法来捕获模块加载中的错误。

promise-loader支持使用第三方Promise基础库(如:bluebird)或者使用global参数来指定使用运行环境已经存在的Promise库。

  • 使用语法:
// 使用Bluebird promise库
var load = require("promise?bluebird!./file.js");

// 使用全局Promise对象
var load = require("promise?global!./file.js");

load().then(function(file) {

});
  • wrapper函数:
var Promise = require('bluebird');

module.exports = function (namespace) {
  return new Promise(function (resolve) {
    require.ensure([], function (require) {
      resolve(require('./file.js')[namespace]));
    });
  });
}

es6-promise-loader

es6-promise-loader相比 promise-loader区别就在于使用原生的ES6 Promise对象。

  • 使用语法:
var load = require("es6-promise!./file.js");

load(namespace).then(function(file) {
    console.log(file);
});
  • wrapper函数:
module.exports = function (namespace) {
  return new Promise(function (resolve) {
    require.ensure([], function (require) {
      resolve(require('./file.js')[namespace]));
    });
  });
}

React按需加载实现方案

React router动态路由

react-router的 标签有一个叫做getComponent的异步的方法去获取组件。他是一个function接受两个参数,分别是location和callback。当react-router执行回调函数 callback(null, ourComponent)时,路由只渲染指定组件ourComponent

  • getComponent异步方法

使用语法:

<Router history={history}>
    <Route 
        path="/"
        getComponent={(nextState, callback) => {
            callback(null, HomePage)
        }}
    />
     <Route
        path="/faq"
        getComponent={(nextState, callback) => {
          callback(null, FAQPage);
        }}
    />
</Router>

这些组件会在需要的时候异步加载。这些组件仍然会在同一个文件中,并且你的应用看起来不会有任何不同。

  • require.ensure

webpack提供的require.ensure可以定义分割点来打包独立的chunk,再配合react-router的getComponent方法就可以实现React组件的按需加载,具体可参照以下文章:

  1. 基于Webpack 2的React组件懒加载
  2. react-router动态路由与Webpack分片thunks

React懒加载组件

文章前面提到使用React动态路由来按需加载react组件,但实际项目开发中很多时候不需要或者没法引入react-router,我们就可以使用webpack官方封装的React懒加载组件来动态加载指定的React组件,源代码见 LazilyLoad。

LazilyLoad懒加载组件

  • LazilyLoad使用:

webpack2.x import方法异步加载ES2015模块文件,返回一个Promise对象。

<LazilyLoad modules={{
  TodoHandler: () => importLazy(import('./components/TodoHandler')),
  TodoMenuHandler: () => importLazy(import('./components/TodoMenuHandler')),
  TodoMenu: () => importLazy(import('./components/TodoMenu')),
}}>
{({TodoHandler, TodoMenuHandler, TodoMenu}) => (
  <TodoHandler>
    <TodoMenuHandler>
      <TodoMenu />
    </TodoMenuHandler>
  </TodoHandler>
)}
</LazilyLoad>

webpack 1.x 使用前文中提到的promise-loader或者es6-promise-loader封装按需加载组件。

class App extends React.Component {

   render() {
        return (
            <div>
                <LazilyLoad modules={{
                    LoadedLate: () => require('es6-promise!./lazy/LoadedLate')(),
                    LoadedLate2: () => require('es6-promise!./lazy/LoadedLate2')()
                }}>
                    {({LoadedLate,LoadedLate2}) => (
                        <div>
                            <LoadedLate />
                            <LoadedLate2 />
                        </div>
                    )}
                </LazilyLoad>
            </div>
        );
    }
  • importLazy方法是为了兼容Babel/ES2015模块,返回模块的default属性。

export const importLazy = (promise) => (
    promise.then((result) => result.default || result)
);

React高阶组件懒加载

高阶组件 (Higher Order Component)就是一个 React 组件包裹着另外一个 React 组件。

这种模式通常使用工厂函数来实现,基本上是一个类工厂。React高阶组件的调用方式与普通的React组件完全一致。

封装懒加载组件LazilyLoad的高阶组件工厂函数

LazilyLoadFactory

export const LazilyLoadFactory = (Component, modules) => {
    return (props) => (
        <LazilyLoad modules={modules}>
            {(mods) => <Component {...mods} {...props} />}
        </LazilyLoad>
    );
};

使用高阶组件实现按需加载

webpack 2.x

class Load_jQuery extends React.Component {

    componentDidMount() {
        console.log('Load_jQuery props:', this.props);
    }

    render() {
        return (
            <div ref={(ref) => this.props.$(ref).css('background-color', 'red')}>
                Hello jQuery
            </div>
        );
    }
};

// 使用工厂函数封装Load_jQuery为高阶组件,将异步加载的jQuery模块对象以props的形式来获取并使用
export default LazilyLoadFactory(Load_jQuery, {
    $: () => import('jquery')
});

webpack 1.x

class Load_jQuery extends React.Component {

   render() {
        return (
            <div ref={(ref) => this.props.$(ref).css('background-color', 'red')}>
                Hello jQuery
            </div>
        );
    }
};

export default LazilyLoadFactory(Load_jQuery, {
    $: () => require('es6-promise!jquery')()
});

ES Decorator

除了工厂函数方式扩展实现高阶组件,还可通过 ES草案中的 Decorator(https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841) 语法来实现。Decorator 可以通过返回特定的 descriptor 来”修饰” 类属性,也可以直接”修饰”一个类。即传入一个已有的类,通过 Decorator 函数”修饰”成了一个新的类。

  • 使用方法:
// ES Decorators函数实现高阶组件封装
// 参考 http://technologyadvice.github.io/es7-decorators-babel6/
const LazilyLoadDecorator = (Component) => {

    return LazilyLoadFactory(Component, {
        $: () => require('jquery')(),
    });
};

// ES Decorators语法
// 需要依赖babel-plugin-transform-decorators-legacy
// babel-loader配置使用plugins: ["transform-decorators-legacy"]
@LazilyLoadDecorator
export default class Load_jQuery extends React.Component {

    componentDidMount() {
        console.log('Load_jQuery props:', this.props);

    }

    render() {
        return (
            <div ref={(ref) => this.props.$(ref).css('background-color', 'red')}>
                Hello jQuery
            </div>
        );
    }

};

引用被高阶组件包裹的普通组件

import Load_jQuery from './js/Load_jQuery';

class App extends React.Component {
    constructor() {
        super(...arguments);
        this.state = {
            load: false,
        };
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        this.setState({
            load: !this.state.load,
        });
    }

    render() {
        return (
            <div>
                <p>
                    <a
                        style={{ color: 'blue', cursor: 'pointer' }}
                        onClick={this.handleClick}>点击加载jQuery</a>
                </p>

                {this.state.load ?
                    <div><Load_jQuery /></div>
                    : null
                }
            </div>
        );
    }

基于webpack 1.x实现react组件的懒加载示例

lazyloaded

参考资料

目录
相关文章
|
缓存 JSON 前端开发
2023年最新前端面试题汇总大全二(含答案超详细,Vue,TypeScript,React,微信小程序,Webpack 汇总篇)-- 持续更新 7
2023年最新前端面试题汇总大全二(含答案超详细,Vue,TypeScript,React,微信小程序,Webpack 汇总篇)-- 持续更新
959 0
|
前端开发 API UED
React 按需加载 Lazy Loading
随着 Web 应用复杂度增加,页面加载速度成为影响用户体验的关键因素。React 提供了按需加载(Lazy Loading)功能,通过 `React.lazy` 和 `Suspense` 实现动态加载组件,减少初始加载时间,提升性能。本文从基础概念入手,探讨常见问题、易错点及解决方案,并通过代码示例详细说明。
760 1
|
前端开发 JavaScript
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
|
前端开发
React按需加载antd步骤以及出现的问题
在使用`babel-plugin-import`插件时,可以在项目的根目录创建`.babelrc`文件或在`package.json`中添加babel配置。这两个文件中不应该存在重复的配置。如果出现"Multiple configuration files found"错误,需要选择其中一个文件进行配置,并删除另一个文件中的babel配置。使用该插件后,可以直接从`antd`引入组件,无需手动引入样式文件。
434 2
|
前端开发 UED 开发者
React.lazy()与Suspense:实现按需加载的动态组件——深入理解代码分割、提升首屏速度和优化用户体验的关键技术
【8月更文挑战第31天】在现代Web应用中,性能优化至关重要,特别是减少首屏加载时间和提升用户交互体验。React.lazy()和Suspense组件提供了一种优雅的解决方案,允许按需加载组件,仅在需要渲染时加载相应代码块,从而加快页面展示速度。Suspense组件在组件加载期间显示备选内容,确保了平滑的加载过渡。
673 0
|
前端开发 JavaScript 容器
前端vw自适应解决方案,适用pc端以及移动端,适用webpack以及vite,适用vue以及react
前端vw自适应解决方案,适用pc端以及移动端,适用webpack以及vite,适用vue以及react
1306 0
|
缓存 前端开发 JavaScript
2023年最新前端面试题汇总大全二(含答案超详细,Vue,TypeScript,React,微信小程序,Webpack 汇总篇)-- 持续更新 5
2023年最新前端面试题汇总大全二(含答案超详细,Vue,TypeScript,React,微信小程序,Webpack 汇总篇)-- 持续更新
525 1
|
前端开发 JavaScript
《Webpack5 核心原理与应用实践》学习笔记-> React全栈环境
《Webpack5 核心原理与应用实践》学习笔记-> React全栈环境
358 0
|
存储 前端开发
React & webpack &ant 学习
React & webpack &ant 学习
215 0
|
前端开发 应用服务中间件 nginx
简单几步,将React项目脚手架Webpack换成Vite⚡⚡,附带性能比较和思考
简单几步,将React项目脚手架Webpack换成Vite⚡⚡,附带性能比较和思考