# webpack4(二) webpack的高级概念

# Tree Shaking

tree shaking 只支持es的方式引入

tree shaking的目的是按需引入

例如

import {add} from './add';

add(1, 2);
1
2
3

如果不使用tree shaking,在打包的时候,会把add.js中的其他导出项也打包进来,这是不必要的。

在development模式下配置tree shaking

module.exports = {
    //...
    optimization: {
        usedExports: true//开发环境里使用tree shaking(按需加载import)
    },
    //...
}
1
2
3
4
5
6
7

配置好optimization后,还有一个问题,在我们引入css或less文件时,是找不到导出的东西的,这个时候tree shaking就会认为这个css文件没有被引用从而不会将其打包进去。 为了解决这个问题还需要在package.json中配置 sideEffects

{
  //...
  "sideEffects": [
    "*.css",
    "*.less"
  ],
  //...
}

1
2
3
4
5
6
7
8
9

在production模式下,自动会有tree shaking

# development 和 production 模式的区分打包

将webpack.config.js拆分为 webpack.dev.js(开发环境) webpack.prod.js(生产环境) webpack.common.js(公共配置) 安装webpack-merge

npm install webpack-merge --save-dev
1

webpack.dev.js

const merge = require('webpack-merge');
const webpack = require('webpack');
const commonConfig = require('./webpack.common');

const devConfig = {
    mode: 'development',
    devtool: 'cheap-module-eval-source-map',
    devServer:{
        contentBase: './dist',
        port: 8080,
        open: true,// 启动服务打开一个网页
        hot: true//热更新
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    optimization: {
        usedExports: true//开发环境里使用tree shaking(按需加载import)
    },

}

module.exports = merge(devConfig, commonConfig);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

webpack.prod.js

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');


const prodConfig = {
    mode: 'production',
    devtool: 'cheap-module-source-map',
}

module.exports = merge(prodConfig, commonConfig);
1
2
3
4
5
6
7
8
9
10

webpack.common.js

const path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    entry: {
        "main": './src/index.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
            },
            {
                test: /\.(jpg|png|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        name: '[name].[ext]',
                        outputPath:'images/',
                        limit: 1024// 图片大小小于限制的时候直接打包到js里,大于的话生成到images/下
                        // 其他用法和file-loader基本一样
                    }
                },
            }, {
                test: /\.less$/,
                use: [
                    //loader执行顺序从下向上
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,// 如果在样式文件里再import进其他的样式文件,则可能不再走下面两个loader,设置importLoaders让他重新再走一遍loader
                            //modules: true,//css模块化加载
                        }
                    },
                    'less-loader',
                    'postcss-loader',//自动增加兼容性前缀
                ],
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
        template: 'src/index.html'
        }), // 打包后在html中自动插入js
        new CleanWebpackPlugin(),// 每次打包之前清除文件
    ],
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, '../dist')
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

我们可以把这些webpack的配置文件都放在一个scripts文件夹下 然后重新配置一下package.json 然后就可以通过命令来分别执行不同模式下的webpack配置了

{
    //...
    "scripts": {
        "start": "webpack-dev-server --config ./scripts/webpack.dev.js",
        "build": "webpack --config ./scripts/webpack.prod.js"
    },
  //...
}
1
2
3
4
5
6
7
8

# Code Splitting 代码分割

先搞个小demo引入一下

import _ from 'lodash'//先引入一个第三方库

// 业务代码
console.log(_.join(['a', 'b', 'c'], '***'))
// 此处省略10万行。。。
console.log(_.join(['a', 'b', 'c'], '***'))
1
2
3
4
5
6

假如引入的第三方库有1mb,业务代码有1mb,那么打包出来的main.js理论上有2mb,打包的文件会很大而且时间会很长。 而且当我们的业务代码进行调整的时候,会重新进行打包,重新访问我们的页面,又要很多时间

一种解决方案是,我们可以在src下再创建一个lodash.js的文件。然后引入lodash库,并将其挂载在window上。这样我们的业务代码和库就分开了。

import _ from 'lodash'//先引入一个第三方库
window._ = _;
1
2

同时我们还要修改webpack配置文件。这样,最后打包出来的就是两个文件了。 此时当业务逻辑改变时,页面只需重新加载main.js文件。

module.exports = {
    entry: {
        "main": './src/index.js',
        "lodash": './src/lodash.js'
    },
}
1
2
3
4
5
6

由此我们引出了code splitting。 在webpack中有配置自动为我们做代码分割。

1.同步代码,只需要在webpack.common.js中配置optimization

module.exports = {
    //...
    optimization: {
        splitChunks: {
            chunks: 'all'// 代码分割
        }
    },
    //...
}
1
2
3
4
5
6
7
8
9

2.异步代码,无需任何配置,会自动进行代码分割

例如在index.js中异步引入

function getComponent() {
    return import('lodash').then(({default: _}) => {
        var element = document.createElement('div');
        element.innerHTML = _.join(['hello', 'webpack'], '--');
        return element;
    })
}

getComponent().then(element => {
    document.body.appendChild(element);
})
1
2
3
4
5
6
7
8
9
10
11

还需要先安装 @babel/plugin-syntax-dynamic-import 插件来使用异步引入

npm install @babel/plugin-syntax-dynamic-import --save-dev
1

在 .babelrc 中添加plugins

{
    //...
    "plugins": ["dynamic-import-webpack"]
}
1
2
3
4

打包可以发现,webpack为我们自动进行了代码分割。

# SplitChunksPlugin

webpack中代码分割底层使用了 SplitChunksPlugin 这个插件

我们在用代码分割的时候会发现,webpack为我们打包出来的文件叫做0.bundle.js而不是我们想要的lodash.js。 接下来就要对SplitChunksPlugin进行配置。

首先在index.js中添加魔法注释

function getComponent() {
    return import(/* webpackChunkName: "lodash" */'lodash').then(({default: _}) => {
        var element = document.createElement('div');
        element.innerHTML = _.join(['hello', 'webpack'], '--');
        return element;
    })
}

getComponent().then(element => {
    document.body.appendChild(element);
})
1
2
3
4
5
6
7
8
9
10
11

此时打包,文件会变为vendors~lodash.bundle.js。 我们还需要继续进行配置

module.exports = {
    //...
    optimization: {
        splitChunks: {
            chunks: 'all', // 如果选择async,则webpack只对异步引入的文件进行代码分割。选择all后则对同时对同步异步进行分割,在对同步代码进行分割的时候,还需要再对cacheGroups进行配置。
            minSize: 30000, // 当模块大于30000时才进行代码分割,小于的话就不进行分割
            maxSize: 0, // 通常不用
            minChunks: 1, // 当文件被用 1 次后进行代码分割
            maxAsyncRequests: 5, // 同时被加载到模块数最多是 5 个
            maxInitialRequests: 3, // 入口文件最多做三个代码分割
            automaticNameDelimiter: '~',// 通过~做连接符
            automaticNameMaxLength: 30,
            name: true,
            cacheGroups: { //缓存组。打包的时候先把所有遇见的模块放在缓存组中,遇到node_modules中的模块放在vendors组中,遇到其他模块放在default组中,在最后一起进行打包
              vendors: {
                test: /[\\/]node_modules[\\/]/,// 当chunks选择all时,执行同步代码分割的话,会先走到这里,对其进行检测是否是node_modules下的库,如果是的话,就将其打包到vendors里,这时我们可以看到打包出来的文件是vendors~lodash.bundle.js
                priority: -10 //值越大优先级越高
              },
              default: {
                // 如果引入的是自定义的文件,而不是node_modules下的文件,此时将会进入default组内
                minChunks: 2,
                priority: -20,
                reuseExistingChunk: true // 如果打包的时候发现一个模块已经被引用打包过了,那么就不再对其进行打包
              }
            }
        }
    },
    //...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# Lazy Loading 懒加载

# 懒加载

在index.js中

function getComponent() {
    return import(/* webpackChunkName: "lodash" */'lodash').then(({default: _}) => {
        var element = document.createElement('div');
        element.innerHTML = _.join(['hello', 'webpack'], '--');
        return element;
    })
}

document.addEventListener('click', () => {
    getComponent().then(element => {
        document.body.appendChild(element);
    })
})
1
2
3
4
5
6
7
8
9
10
11
12
13

当刷新页面时,只加载了html和main.js,当点击之后才加载lodash.js 这用的就是懒加载

懒加载就是当需要用的时候才会去加载对应的文件,以此来提高运行速度

# chunks

当使用代码分割时,打包过后会被拆分成多个文件,每个被拆分的文件都是一个chunk

# 打包分析 preloading prefetching

# 打包分析

对我们打包生成的文件进行分析

打包分析

首先先生成打包描述文件

在package.json中添加 --profile --json > stats.json 从而在打包的过程中生成一个stats.json文件

{
  //...
  "scripts": {
    "bundle": "webpack --profile --json > stats.json --config ./scripts/webpack.dev.js",
  },
  //...
}
1
2
3
4
5
6
7

然后在刚才的网站中点击analyse会跳转到网站 分析的地方

# preloading prefetching

为了提高页面性能,我们通常使用缓存来进行提高,其实我们更应该关注的是代码利用率。 在一些交互代码中,webpack建议我们使用异步加载来提高性能 可以在控制台中使用coverage来查看

例如,当点击时加载

function getComponent() {
    var element = document.createElement('div');
    element.innerHTML = 'Hello webpack';
    document.body.appendChild(element);
}
1
2
3
4
5

我们可以进行如下优化

在click.js中

function handlclick() {
    var element = document.createElement('div');
    element.innerHTML = 'Hello webpack';
    document.body.appendChild(element);
}
export default handlclick;
1
2
3
4
5
6

在index.js中

document.addEventListener('click', () => {
    import('./click.js').then(({default: _}) => {
        _();
    })
})
1
2
3
4
5

这样能够提高我们的代码使用率

那么如何能够更好的提高性能呢 当然是在核心代码加载完后,趁着空余的带宽将暂时不用的文件代码加载进来 这就是preloading 和 prefetching 要做的事情

在index.js中添加魔法注释,使用 prefetching

document.addEventListener('click', () => {
    import(/* webpackPrefetching: true */'./click.js').then(({default: _}) => {
        _();
    })
})
1
2
3
4
5

# CSS文件的代码分割

首先安装 mini-css-extract-plugin

npm install mini-css-extract-plugin --save-dev
1

mini-css-extract-plugin插件只对生产环境有效 所以我们需要更改webpack.prod.js文件 引入 mini-css-extract-plugin 插件 然后将module的关于样式的部分的loader进行替换,使用 MiniCssExtractPlugin.loader 同时也要把公共的部分的loader剔除掉以免发生报错

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')// 新增;

const prodConfig = {
    //...
    module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    //loader执行顺序从下向上
                    MiniCssExtractPlugin.loader,// 使用css代码分割
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,// 如果在样式文件里再import进其他的样式文件,则可能不再走下面两个loader,设置importLoaders让他重新再走一遍loader
                            //modules: true,//css模块化加载
                        }
                    },
                    'less-loader',
                    'postcss-loader',//自动增加兼容性前缀
                ],
            }, {
                test: /\.css$/,
                use: [
                    //loader执行顺序从下向上
                    MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1,// 如果在样式文件里再import进其他的样式文件,则可能不再走下面两个loader,设置importLoaders让他重新再走一遍loader
                            //modules: true,//css模块化加载
                        }
                    },
                    'postcss-loader',//自动增加兼容性前缀
                ],
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
          filename: "[name].css",
          chunkFilename: "[id].css"
        })
      ],
    //...
}

module.exports = merge(prodConfig, commonConfig);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

接下来我们想让打包好的css文件直接进行代码压缩

npm install --save-dev optimize-css-assets-webpack-plugin
1

引入插件

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');// 新增;
const prodConfig = {
    //...
    optimization: {
        minimizer: [new OptimizeCSSAssetsPlugin({})],
    },
    //...
}

module.exports = merge(prodConfig, commonConfig);
1
2
3
4
5
6
7
8
9
10
11
12
13

# webpack与浏览器缓存(caching)

当我们的代码放在服务器上供用户使用时,用户再次刷新页面时,会使用缓存进行读取,可以提高速度。但是如果我们的代码发生变化,但是打包出来的名字还是原来的,这时用户还是会读取缓存里的内容。 这样不好。 做一下改善 在webpack.prod.js中给output加入contenthash占位符 同时修改dev和common文件

const prodConfig = {
    //...
    output: {
        filename: '[name].[contenthash].js',
        chunkFilename: '[name].[contenthash].js'
    },
}
1
2
3
4
5
6
7

这样当文件没有改变时,打包出来的文件名的hash值不会变,即用户可以使用缓存来进行访问。当文件内容发生改变,hash值也会进行变化,用户就会重新加载文件访问。以此来提高效率。

# shimming 垫片

当我们在引入其他人的文件或者库时,例如里面用了jquery,但是在他的文件里面有没有引入jquery(import $ from jquery),这个时候会报错。即便我们在外面引用这个文件的地方引入了$ 依然会报错。 此时就用到了shimming 我们在webpack.common.js中引入webpack,然后引用插件 new webpack.ProvidePlugin({}) 我们希望在遇到$时自动去加载jquery,而不需要我们手动去加载这个库

var webpack = require(webpack);
module.exports = {
    //...
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery'// 垫片,在遇到"$"时,就会自动加载jquery库
        })
    ],
    //...
}
1
2
3
4
5
6
7
8
9
10

我们在模块中输出this,发现this并没有指向window,如果我们想让this指向window我们需要再安装一个loader

npm install imports-loader --save-dev
1

然后修改webpack.common.js 这样就可以指向window了

module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {loader: "babel-loader"},
                    {loader: "imports-loader?this=>window"},
                ]
            },
        ]
    }
    //...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
更新时间: 11/8/2019, 4:51:43 PM