概述

通过webpack实现前端项目整体模块化的优势固然很明显,但是他同样存在一些笔端,那就是我们项目当中所有的代码最终都会被打包到一起,那试想一下如果说我们的应用非常复杂,模块非常多的话那我们的打包结果就会特别的大,很多时候超过2-3M也是非常常见的事。

而实施情况是,大多数时候我们在应用开始工作时并不是我们所有的模块都是必须要加载进来的。但是呢,这些模块又被全部打包到一起,那我们需要任何一个模块都必须把整体加载下来过后才能使用。

而我们的应用呢,一般又是运行在浏览器端,那这就意味着我们会浪费掉很多的流量和带宽,那更为合理的方案呢就是把我们的打包结果按照一定的规则去分离到多个bundle当中,然后根据我们应用的运行需要按需去加载这些模块。

这样的话我们就可以大大提高我们应用的响应速度以及他的运行效率。

那可能有人会想起来我们在一开始的时候说过,webpack就是把我们项目中散落的模块合并到一起从而去提高运行效率。

那我们这里又在说他应该把他分离开,那这两个说法是不是自相矛盾呢,其实这并不是矛盾,只是物极必反而已。

那资源太大了不行,太碎了也不行,我们项目中划分的这种模块的颗粒度一般都会非常的细,那很多时候我们一个模块只是提供了一个小小的工具函数,它并不能形成一个完整的功能单元。

那如果我们不把这些散落的模块合并到一起,那就有可能我们再去运行一个小小的功能时就会加载非常多的模块。

而我们目前这种主流的HTTP1.1版本,它本身就有很多缺陷,例如我们并不能同时对同一个域名下发起很多次的并行请求,而且我们一次的请求呢他都会有一定的延迟。

另外我们每次请求除了传输具体的内容以外还会有额外的请求头和响应头,那当我们有大量的这种请求情况下,那这些请求头响应头加在一起也是很大的浪费。

那综上所述,模块化打包肯定是有必要的,不过呢,在我们的应用越来越大过后,我们也要慢慢的开始学会变通。

那为了解决这样的问题,webpack呢他支持一种分包的功能,你也可以把这种功能称之为代码分割。

他通过把我们的模块按照我们所设计的一个规则打包到不同的bundle当中,从而去提高我们应用的响应速度。

目前呢webpack去实现分包的方式主要有两种,那第一种就是我们根据我们的业务去配置不同的打包入口,也就是我们会有同时多个打包入口同时打包,那这时候就会输出多个打包结果。

那第二种呢就是采用ES Module的动态导入这样一个功能去实现模块的按需加载,那这个时候呢,我们webpack他也会自动的把我们动态导入的这个模块单独的输出到一个bundle当中。

那接下来我们来具体来看这两种方式。

多入口打包

多入口打包一般适用于传统的多页应用程序,那最常见的划分规则就是一个页面去对应一个打包入口。

那对于不同页面之间的公共部分,再去提取到公共的结果当中。

那这种方式呢使用起来非常简单。我们回到项目中具体来看。我们准备了一个多页应用的示例,我们这里有两个页面,分别是index和album页面。

那代码的组织逻辑也非常简单,index.js负责实现index页面所有功能。而album.js负责实现album页面所有功能。

global.css和fetch.js都是公共部分,下面我们尝试为这个案例配置多个打包入口。

一般我们配置文件当中的entry属性他只会配置一个文件名路径,也就是说我们只会配置一个打包入口,那如果我们需要配置多个入口的话,我们可以把entry定义成一个对象,那需要注意的是这里是一个对象,而不是数组。

因为如果定义成数组的话,那他就是把多个文件打包到一起,那对于整个应用来讲的话还是一个入口。

那我们这里需要的是多入口所以我们配置成一个对象。那在这个对象中一个属性就是一路入口。那我们属性名就是这个入口的名称。然后值就是这个入口所对应的文件路径。

那我们这里配置的就是index和album这两个js所对应的文件路径,那一但我们这里配置为多入口,那我们输出的文件名也需要修改,那这俩两个入口也就意味着会有两个打包结果,我们不能都叫bundle.js。

所以说我们这里可以为我们filename属性去添加一个[name]这种占位符的方式来去动态输出文件名。

那么[name]最终就会被替换成入口的名称,那在我们这就是index和album。

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = allModes.map(item => {
    return {
        mode: 'none',
        entry: {
            index: './src/index.js',
            album: './src/album.js',
        },
        output: {
            filename: `[name].bundle.js`,
        },
        module: {
            rules: [
                {
                    test: /\.css$/,
                    use: [
                        'style-loader',
                        'css-loader'
                    ]
                }
            ]
        },
        plugins: [
            new CleanWebpackPlugin(),
            new HtmlWebpackPlugin({
                template: './src/index.html',
                filename: `index.html`
            })
            new HtmlWebpackPlugin({
                template: './src/album.html',
                filename: `album.html`
            })
        ]
    }
})

那我们这次打包就会有两个入口,完成以后我们找到输出的目录,那在输出的目录我们就能看到两个入口各自打包过后的结果了。

但是这里呢,还是会有一个小问题,我们打开任意一个输出的html文件,这时候你就会发现这两个打包结果他都被页面同时载入了。

而我们希望的是,一个页面只使用他对应的那个输出结果,所以说这里我们还需要继续去修改配置文件。

那我们回到配置文件当中,我们找到输出html的插件, 那之前我们就介绍过,这个插件他默认就会输出一个自动注入所有打包结果的html。

[
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
        template: './src/index.html',
        filename: `index.html`
    })
    new HtmlWebpackPlugin({
        template: './src/album.html',
        filename: `album.html`
    })
]

那如果说我们需要指定我们输出的html他所使用的bundle, 那我们就可以使用chunk属性来去设置。

那我们每一个打包入口呢他就会形成一个独立的chunk,那我们在这分别为这两个页面配置不同的chunk。

[
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
        template: './src/index.html',
        filename: `index.html`,
        chunk: ['index']
    })
    new HtmlWebpackPlugin({
        template: './src/album.html',
        filename: `album.html`,
        chunk: ['album']
    })
]

完成以后再次重新打包,那我们这次的打包结果呢他就完全正常了。

那以上就是我们配置多入口的打包方式,以及我们如何在输出的html当中指定我们需要注入的bundle。

提取公共模块

多入口打包本身非常容易理解也非常容易使用,但是他也存在一个小小的问题,那就是我们再不同的打包入口当中他一定会有那么一些公共的部分。

那按照之前这种多入口的打包方式,就会出现我们再不同的打包结果当中会有相同的模块出现。例如在我们这里index入口和album入口中就共同使用了global.css和fetch.js这两个公共的模块。

那这里是因为我们的示例比较简单,所以说重复的影响不会有那么大,但是如果说我们共同使用的是jQuery或者是Vue这种体积比较大的模块,那影响的话就会特别的大。

所以说我们需要把这些公共的模块去提取到一个单独的bundle当中。

那webpack当中实现公共模块提取的方式也非常简单,我们只需要在优化配置当中去开启一个叫做splitChunks的一个功能就可以了。

我们回到配置文件当中,我们再optimization中添加splitChunks属性,那这个属性他需要我们配置一个chunks属性,然后我们将这个chunks属性设置为all,就表示我们会把所有的公共模块都提取到单独的bundle当中。

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = allModes.map(item => {
    return {
        mode: 'none',
        entry: {
            index: './src/index.js',
            album: './src/album.js',
        },
        output: {
            filename: `[name].bundle.js`,
        },
        optimization: {
            splitChunks: {
                chunks: 'all'
            }
        }
        module: {
            rules: [
                {
                    test: /\.css$/,
                    use: [
                        'style-loader',
                        'css-loader'
                    ]
                }
            ]
        },
        plugins: [
            new CleanWebpackPlugin(),
            new HtmlWebpackPlugin({
                template: './src/index.html',
                filename: `index.html`,
                chunk: ['index']
            })
            new HtmlWebpackPlugin({
                template: './src/album.html',
                filename: `album.html`,
                chunk: ['album']
            })
        ]
    }
})

打包过后我们的dist目录下就会生成额外的一个js文件,在这个文件当中就是我们index和album这两个入口公共的模块部分了。

动态导入

按需加载是我们开发浏览器应用当中一个非常常见的需求,那一般我们常说的按需加载指的是加载数据。那我们这里所说的按需加载呢,指的是我们再应用运行过程中需要某个模块时我们才去加载这个模块。

那这种方式呢,可以极大地节省我们的带宽和流量。

那webpack中支持使用动态导入的这种方式来去实现模块的按需加载,而且呢,所有动态导入的模块都会自动被提取到单独的bundle中,从而实现分包,

那相比于多入口的这种方式动态导入他更为灵活,因为我们可以通过代码的逻辑去控制我们需不需要加载某个模块,或者是我们什么时候加载某个模块。

而我们分包的目的中就有很重要的一点就是,要让模块实现按需加载,来提高应用的响应速度。我们具体来看如何使用。

这里我们已经设计好了一个可以发挥按需加载作用的场景,在这个页面的主体区域,如果我们访问的是文章页的话,我们得到的就是一个文章列表,如果我们访问的是相册页面,我们显示的就是相册列表。

回到代码当中我们来看下他的实现方式,目前我们文章列表所对应的就是post组件,而相册列表对应的就是album组件,我们在打包入口当中同时导入这两个模块。

然后这里的逻辑就是当我们锚点发生变化时,我们去根据锚点的值决定要去显示哪个组件。

import posts from './posts/posts';
import album from './album/album';

const render = () => {
    const hash = locaton.hash || '#posts';

    const mainElement = document.querySelector('.main');

    mainElement.innerHTML = '';

    if (hash === '#posts') {
        mainElement.appendChild(post());
    } else if (hash === '#album') {
        mainElement.appendChild(album());
    }
}

render();

window.addEventListener('hashchange', render);

那这里就会存在浪费的可能性,是想一下,如果说用户他打开我们的应用过后只是访问了其中的一个页面。那另外一个页面所对应的这个组件的加载就是浪费。

所以说我们这里如果是动态导入组件,那就不会存在浪费的问题了。我们这里可以先注释掉静态导入。

动态导入使用的就是ES Module标准当中的动态导入,我们在需要动态导入的地方通过import这个函数,然后导入我们指定的路径。

那这个方法返回的就是一个Promise,然后在这个Promise的then方法中我们就可以拿到模块对象。

由于我们这里使用的是默认导出,所以我们这需要解构我们模块对象的default,我们把它放在posts这个变量当中,拿到这个成员过后我们再来使用这个成员去创建界面上的元素。

同理我们的album组件也应该是如此。

// import posts from './posts/posts';
// import album from './album/album';

const render = () => {
    const hash = locaton.hash || '#posts';

    const mainElement = document.querySelector('.main');

    mainElement.innerHTML = '';

    if (hash === '#posts') {
        // mainElement.appendChild(posts());
        import('./posts/posts').then(({ default: posts}) => {
            mainElement.appendChild(posts());
        })
    } else if (hash === '#album') {
        // mainElement.appendChild(album());
        import('./album/album').then(({ default: album}) => {
            mainElement.appendChild(album());
        })
    }
}

render();

window.addEventListener('hashchange', render);

完成以后我们在此回到浏览器,此时我们页面仍然可以正常工作,我们回到开发工具当中,重新运行打包,然后去看看此时我们打包的结果是什么样子的。

打包完成过后我们打开输出的dist目录,在此时我们的dist目录下就会多出三个js文件,那这三个js文件呢实际上就是由动态导入,自动分包所产生的。

那这三个文件分别是我们刚刚导入的两个模块以及这两个模板当中公共的部分所提取出来的bundle, 那这就是动态导入在webpack当中的一个使用。

那整个过程我们无需配置任何一个地方只需要按照ES Module动态导入成员的方式去导入模块就可以了。那webpack内部呢会自动处理分包和按需加载。

那如果说你使用的是单页应用开发框架,比如react或者vue的话,那在你项目当中的路由映射组件就可以通过这种动态导入的方式实现按需加载。

魔法注释

默认通过动态导入产生的bundle文件,他的名称就只是一个序号,这并没有什么不好的,因为在生产环境当中,大多数时候我们是根本不用关心资源文件的名称是什么。

但是说如果你还是需要给这些bundle命名的话,那你可以使用webpack所特有的模板注释来去实现。

那具体的使用方式就是在调用import函数的参数位置我们去添加一个行内注释,那这个注释有一个特定的格式,就是通过/* webpackChunkName: 名称 */ 那这样的话我们就可以给分包所产生的bundle起上名字了。

// import posts from './posts/posts';
// import album from './album/album';

const render = () => {
    const hash = locaton.hash || '#posts';

    const mainElement = document.querySelector('.main');

    mainElement.innerHTML = '';

    if (hash === '#posts') {
        // mainElement.appendChild(posts());
        import(/* webpackChunkName: posts */'./posts/posts').then(({ default: posts}) => {
            mainElement.appendChild(posts());
        })
    } else if (hash === '#album') {
        // mainElement.appendChild(album());
        import(/* webpackChunkName: album */'./album/album').then(({ default: album}) => {
            mainElement.appendChild(album());
        })
    }
}

render();

window.addEventListener('hashchange', render);

我们重新打包,那我们生成的bundle文件他的name就会使用我们刚刚注释当中所提供的名称了。

那如果说你的chunkName是相同的话,那相同的chunkName最终就会被打包到一起。那例如我们这里可以把这两个chunk的chunkName设置为components, 这样的话他们就一致了。然后我们再次运行打包。

// import posts from './posts/posts';
// import album from './album/album';

const render = () => {
    const hash = locaton.hash || '#posts';

    const mainElement = document.querySelector('.main');

    mainElement.innerHTML = '';

    if (hash === '#posts') {
        // mainElement.appendChild(posts());
        import(/* webpackChunkName: components */'./posts/posts').then(({ default: posts}) => {
            mainElement.appendChild(posts());
        })
    } else if (hash === '#album') {
        // mainElement.appendChild(album());
        import(/* webpackChunkName: components */'./album/album').then(({ default: album}) => {
            mainElement.appendChild(album());
        })
    }
}

render();

window.addEventListener('hashchange', render);

那此时呢这两个模块他都会被打包到components.bundle.js这样一个文件当中。

那借助于这样一个特点,你就可以根据自己的实际情况灵活组织我们动态加载的模块所输出的文件了。