# 高级概念

# 一、Tree Shaking

  • 作用:打包的时候把一个模块里面没有用到的东西全部移除掉。
  • 注意:Tree Shaking只支持ES Module的引用方式,因为Tree Shaking只支持静态引入。ES Module属于静态引入,而CommonJS属于动态引入

先来看一个例子

// index.js
import { add } from './math';
add(1,2);

// math.js
export const add = (a, b) => {
    console.log(a + b);
}

export const minus = (a, b) => {
    console.log(a - b);
}
1
2
3
4
5
6
7
8
9
10
11
12

打包的时候会发现,虽然index.js只引入了add方法,但是打包后的bunlde.js把minus方法也包含在里面了,这样会使bunlde.js的体积变大,怎么优化?就是利用Tree Shaking,把没有使用到的方法或者模块全部剔除掉,不打包。

webpack配置:

// webpack.config.js
plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    }),
    new CleanWebpackPlugin(),
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin()
],
//tree shaking 配置,开发模式下需要配置,生产模式不需要,默认会tree shaking
optimization: {
    usedExports: true  // 只使用导出的模块
},
1
2
3
4
5
6
7
8
9
10
11
12
13
  • sideEffects选项

如果不需要对某个模块作tree shaking,则需要在package.json作配置,例如下面的@babel/polyfill和index.css,由于没有导出任何东西,那么设置了tree Shaking的情况下,就会被tree Shaking剔除掉,这不是我们希望的。

import '@babel/polyfill';  // 默认会被tree Shaking
import './index.css';  // 默认会被tree Shaking
import { add } from './math';
1
2
3

修改package.json:

// package.json部分配置
{
    "name": "learn-webpack",
    "sideEffects": [   // 写入不需要被tree Shaking的模块,开发和生产环境都需要配置
      "@babel/polyfill",
      "*.css"  // 所有css保留,不作tree Shaking
    ],
    "version": "1.0.0",
}
1
2
3
4
5
6
7
8
9

如果没有不需要被tree Shaking的模块,则把"sideEffects"设置为false即可。

注意:在development开发模式下,打包后的代码只会标记没有使用到的模块,并不会移除掉没有使用的方法或者模块,因为开发环境是需要调试的,移除的话不利于调试。只有在production模式下才会被真正移除。 另外生产模式默认会tree shaking,不需要做webpack.config.js中的optimization配置,package.json按照需要填写"sideEffects"选项即可。

# 二、区分开发模式和生产模式

开发模式和生产模式的打包区别:

  • sourceMap
  • 压缩代码

把开发模式和生产模式公用的配置写在一个文件里,然后利用webpack-merge模块实现合并配置。

安装webpack-merge,npm i -D webpack-merge,webpack配置如下:

  • webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
    //entry: './src/index.js', // 入口文件,等价于下面的main
    entry: {
        main: './src/index.js', // 第一个入口文件
        sub: './src/index.js', // 第二个入口文件
    },
    // 打包出口
    output: {
        //publicPath: '/', // webpackDevServer要配置这个
        // publicPath: 'http://cdn.com/',  // 在打包后生成的js地址前面加上域名,即http://cdn.com/main.js
        filename: '[name].js', // 打包后的文件的名字,[name]是占位符,是上面的entry里的main或者sub
        path: path.resolve(__dirname, 'dist') // 打包后的目录,必须使用绝对路径,要用到path.resolve
    },
    // 配置相关loader
    module: {
        rules: [
            {
                test: /\.(jpg|png|jpeg)$/,
                use: {
                    // loader: 'file-loader',  // 使用 file-loader 打包
                    // options: {
                    //     name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                    //     outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                    // }
                    loader: 'url-loader', // 使用 url-loader 打包,在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
                    options: {
                        name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                        outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                        limit: 2048  // 图片小于2048字节就会打包成base64
                    }
                }
            },
            {
                test: /\.(css|scss)$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
            {
                test: /\.(eot|ttf|svg)$/,
                use: {
                    loader: 'file-loader'  // 使用 file-loader 打包
                }
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {
                    // 开发业务代码时的写法
                   //presets: ["@babel/preset-env"]
                   presets: [['@babel/preset-env', {
                       targets: {
                           edge: "17",
                           firefox: "60",
                           chrome: "67",
                           safari: "11.1",
                       },
                       useBuiltIns: 'usage',
                       corejs: "2", // 声明corejs版本
                   }]]
                    // 开发第三方模块时的写法
                    /*
                    "plugins": [
                        [
                            "@babel/plugin-transform-runtime",
                            {
                                "absoluteRuntime": false,
                                "corejs": 2,
                                "helpers": true,
                                "regenerator": true,
                                "useESModules": false
                            }
                        ]
                    ]
                    */
                }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin()
    ]
}
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
  • webpack.dev.js
const webpack = require('webpack');
const commonConfig = require('./webpack.common.js');
const merge = require('webpack-merge');  // 合并配置插件

const devConfig = {
    mode: 'development', // 开发模式
    //devtool: 'source-map', // 设置为none时无法定位语法错误发生在源代码里第几行
    devtool: 'cheap-module-eval-source-map',  // 开发模式最佳配置
    plugins: [
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
    ],
    optimization: {
        usedExports: true  // tree shaking 配置,开发模式下需要配置,生产模式不需要,默认会tree shaking
    },
    devServer: {  // 配置devServer
        contentBase: './dist',  // 打包后起服务的目录名
        open: true,   // 自动打开浏览器
        proxy: {
            '/api': 'http://localhost:3000'  // 跨域代理转发
        },
        port: 8090,  // 指定端口号
        hot: true,  // 开启热模块替换
        hotOnly: true  // 热模块替换没有生效,则让浏览器自动刷新
    }
}

// 结合
module.exports = merge(commonConfig, devConfig);
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
  • package.json
"scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "prod": "webpack --config ./build/webpack.prod.js"
}
1
2
3
4

# 三、Code Splitting 代码分割

webpack默认会打包成一个bundle.js,打包出来的文件会很大,加载时间会很长,另外像loadash这样的第三方库是不需要打包的。

# 优化方式

  • 方法1:手动抽离第三方模块,使用多入口进行打包(不推荐)
// index.js
console.log(_.join(['a','b'],''))

// loadash.js
import _ from 'lodash';
window._ = _;

// webpack.config.js
entry: {
    loadsh: './src/loadsah.js',
    main: './src/index.js', // 第一个入口文件
},
1
2
3
4
5
6
7
8
9
10
11
12
  • 方法2:用webpack4自带的插件配置(支持同步引入)

由于webpack4 天然支持动态import的模块的code Splitting,所以对于动态import的模块不需要配置任何东西。

// index.js
function getComponet() {
    // 异步载入语法import(),webpack4已经支持code Splitting,不需要任何配置
    return import('lodash').then(({ default: _ }) => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['a', 'b'], ',');
        return ele;
    })
}

getComponet().then(ele => {
    document.body.appendChild(ele);
})
1
2
3
4
5
6
7
8
9
10
11
12
13

如果是通过import xxx from xxx这样静态引入的模块,则还需要配置splitChunks参数。

// webpack.config.js
plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    }),
    new CleanWebpackPlugin()
],
optimization: {
    // ********Code Splitting配置,支持静态引入的模块********
    splitChunks: {
        chunks: 'all'
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# splitChunks配置

针对动态引入的import的打包,可以配置更多参数,比如修改生成的chunk的名字。安装一个插件,npm install --save-dev @babel/plugin-syntax-dynamic-import

.babelrc配置:

{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}
1
2
3

修改打包后的名字

// index.js
function getComponet() {
    return import(/* webpackChunkName:"loadash" */'lodash').then(({ default: _ }) => {
        var ele = document.createElement('div');
        ele.innerHTML = _.join(['a', 'b'], ',');
        return ele;
    })
}
1
2
3
4
5
6
7
8

/* webpackChunkName:"loadash" */ 意思是为打包生成的loadash起一个名字。默认动态import生成的名字是 数字.js。这样配置后,打包后的名字会变成vender-loadash.js,如果不需要带vender,则需要修改webpack配置:

 optimization: {
    // Code Splitting配置
    splitChunks: {
        chunks: 'all',
        cacheGroups: {
            vendors: false,
            default: false
        }
    }
}
1
2
3
4
5
6
7
8
9
10

splitChunks配置官方文档

// 全部配置
optimization: {
    // Code Splitting配置
    splitChunks: {
        // chunks: 'all',
        // cacheGroups: {
        //     vendors: false,
        //     default: false
        // }
        chunks: "all",  // async,只对异步代码进行code spliting,all就是同步异步都分割
        minSize: 30000, // 代码分割最小的文件大小
        minChunks: 1, // 至少有1个chunk引用了这个模块,才会code Spliting
        maxAsyncRequests: 5,
        maxInitialRequests: 3,
        automaticNameDelimiter: '~',
        name: true,
        cacheGroups: {
            // vendors
            vendors: {
                test: /[\\/]node_modules[\\/]/,  // 代码必须在node_module里面才会分
                // 到这个vendors组
                priority: -10,  // 打包的权重,会选择priority大的打包
                filename: "vendors.js"  // 仅对同步引入的模块生效,修改文件名
            },
            default: {
                minChunks: 2,
                priority: -20,
                reuseExistingChunk: true, // 如果一个模块被打包过了,则直接复用,
                // 不再做打包
                filename: "common.js" // 修改文件名,仅对同步引入的模块生效
            }
        }
    }
}
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

# 四、Lazy Loading 懒加载

动态import()方法引入的模块会有懒加载的功能。打包后的每个js文件都是一个chunk。

# 五、打包分析、preloading、prefetching

# 打包分析

webpack --profile --json > stats.json --config ./build/webpack.dev.js"
1

在webpack命令后面加--profile --json > stats.json

# preloading、prefetching

举个例子,比如下面这样的代码

// index.js
document.addEventListener('click', () => {
    const ele = document.createElement('div');
    ele.innerHTML = 'hello';
    document.body.appendChild(ele);
});
1
2
3
4
5
6

点击后添加一个节点。其实我们可以把添加节点的逻辑通过异步import的方式进行优化,即异步加载(即懒加载)。

// index.js
document.addEventListener('click', () => {
    import('./click.js').then(({ default: _ }) => {
        _();
    })
});

// click.js
function handClick() {
    const ele = document.createElement('div');
    ele.innerHTML = 'hello';
    document.body.appendChild(ele);
}
export default handClick;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这样异步加载,减少了bundle的体积大小。但是异步加载也有一个问题,就是异步加载的时候可能会比较慢,特别是有图片的时候,用户体验不好。解决办法是,在网络空闲的时候进行异步加载。

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

/* webpackPrefetch:true */ 意思就是在网络空闲的时候去异步加载。

# 六、CSS文件代码分割

# 分离CSS

安装css代码分割插件,npm install --save-dev mini-css-extract-plugin

webpack配置:

// webpack.prod.js
const commonConfig = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const merge = require('webpack-merge'); // 合并配置插件
const prodConfig = {
    mode: 'production', // 生产模式
    devtool: 'cheap-module-source-map',  //生产模式最佳配置
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[id].css',
        }),
    ],
    module: {
        rules: [
            {
                test: /\.(css|scss)$/,
                use: [
                    MiniCssExtractPlugin.loader,  //  把style-loader换成MiniCssExtractPlugin.loader
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
        ]
    },
    optimization: {
        //tree shaking 配置
        usedExports: true
    }
}
// 结合
module.exports = merge(commonConfig, prodConfig);
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

package.json配置:

{
  "name": "learn-webpack",
  "sideEffects": [
    "*.scss",
    "*.css"
  ],
}
1
2
3
4
5
6
7

注意package.json也要作配置。这个插件用于生产环境。

# 压缩CSS和JS

安装压缩css和js的插件,npm i -D optimize-css-assets-webpack-plugin terser-webpack-plugin

webpack.prod.js配置

const commonConfig = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const merge = require('webpack-merge'); // 合并配置插件
const prodConfig = {
    mode: 'production', // 生产模式
    devtool: 'cheap-module-source-map',  //生产模式最佳配置
    plugins: [
        // 分离css
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[id].css',
        })
    ],
    module: {
        rules: [
            {
                test: /\.(css|scss)$/,
                use: [
                    MiniCssExtractPlugin.loader,  //  把style-loader换成MiniCssExtractPlugin.loader
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
        ]
    },
    optimization: {
        // 压缩css和js
        minimizer: [
            new TerserJSPlugin({}), // 压缩混淆js
            new OptimizeCSSAssetsPlugin({})  // 压缩css
        ]
    }
}

// 结合
module.exports = merge(commonConfig, prodConfig);
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

注意,不使用这两个插件的情况下,JS会压缩,CSS不会压缩。

# 合并CSS

optimization: {
    //压缩css和js
    minimizer: [
        new TerserJSPlugin({}), // 压缩混淆js
        new OptimizeCSSAssetsPlugin({})  // 压缩css
    ],
    // 将所有css合并到一个css里面
    splitChunks: {
        cacheGroups: {
            styles: {
                name: 'styles',
                test: /\.css$/,
                chunks: 'all',
                enforce: true,
            }
        },
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

参考

# 七、webpack与浏览器缓存

浏览器缓存。生产环境下配置contenthash,这样当重新打包的时候,用户就只会重新加载发生改变了的文件,没有改变的文件则使用缓存。

 // 生产环境下,需要这么写,因为缓存的问题
output: {
    filename: '[name].[contenthash].js', // 打包后的文件的名字,[name]是占位符,
    // 是上面的entry里的main或者sub
    chunkFilename: '[name].[contenthash].js',
}
1
2
3
4
5
6

# 八、Shimming

Shimming的作用:代码兼容或者打包过程的兼容,解决兼容的问题。

plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    }),
    new CleanWebpackPlugin(),
    // 当任何一个模块中用了$,就自动加载juqery模块,并导入命名为$   
    new webpack.ProvidePlugin({
       $:'jquery',
       _:'lodash',
       _join:['loadsh','join']           
    })
]
1
2
3
4
5
6
7
8
9
10
11
12

# 九、环境变量的使用

使用环境变量前:

//webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
    //entry: './src/index.js', // 入口文件,等价于下面的main
    entry: {
        main: './src/index.js' // 第一个入口文件
    },
    performance: false,
    // 打包出口
    output: {
        //publicPath: '/',  // webpackDevServer要配置这个
        // publicPath: 'http://cdn.com/',  // 在打包后生成的js地址前面加上域名,即http://cdn.com/main.js
        path: path.resolve(__dirname, '../dist') // 打包后的目录,必须使用绝对路径,要用到path.resolve
    },
    // 配置相关loader
    module: {
        rules: [
            {
                test: /\.(jpg|png|jpeg)$/,
                use: {
                    // loader: 'file-loader',  // 使用 file-loader 打包
                    // options: {
                    //     name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                    //     outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                    // }
                    loader: 'url-loader', // 使用 url-loader 打包,在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
                    options: {
                        name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                        outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                        limit: 2048  // 图片小于2048字节就会打包成base64
                    }
                }
            },
            {
                test: /\.(eot|ttf|svg)$/,
                use: {
                    loader: 'file-loader'  // 使用 file-loader 打包
                }
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {
                    // 开发业务代码时的写法
                    //presets: ["@babel/preset-env"]
                    presets: [['@babel/preset-env', {
                        targets: {
                            edge: "17",
                            firefox: "60",
                            chrome: "67",
                            safari: "11.1",
                        },
                        useBuiltIns: 'usage',
                        corejs: "2", // 声明corejs版本
                    }]]
                    // 开发第三方模块时的写法
                    /*
                    "plugins": [
                        [
                            "@babel/plugin-transform-runtime",
                            {
                                "absoluteRuntime": false,
                                "corejs": 2,
                                "helpers": true,
                                "regenerator": true,
                                "useESModules": false
                            }
                        ]
                    ]
                    */
                }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin(),
        // 当任何一个模块中用了$,就自动加载juqery模块,并导入命名为$   
        new webpack.ProvidePlugin({
           $:'jquery',
           _:'lodash',
           _join:['loadsh','join']           
        })
    ],
    optimization: {
        //tree shaking 配置
        usedExports: true,  // 开发模式下需要配置,生产模式不需要,默认会tree shaking
        // Code Splitting配置
        splitChunks: {
            // chunks: 'all',
            // cacheGroups: {
            //     vendors: false,
            //     default: false
            // }
            chunks: "all",  // async,只对异步代码进行code spliting,all就是同步异步都分割
            minSize: 30000, // 代码分割最小的文件大小
            minChunks: 1, // 至少有1个chunk引用了这个模块,才会code Spliting
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
                // vendors
                vendors: {
                    test: /[\\/]node_modules[\\/]/,  // 代码必须在node_module里面才会分到这个vendors组
                    priority: -10,  // 打包的权重,会选择priority大的打包
                    //filename: "vendors.js"  // 仅对同步引入的模块生效,修改文件名
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true, // 如果一个模块被打包过了,则直接复用,不再做打包
                    //filename: "common.js" // 仅对同步引入的模块生效,修改文件名
                }
            }
        }
    }
}



//  webpack.dev.js
const webpack = require('webpack');
const commonConfig = require('./webpack.common.js');
const merge = require('webpack-merge');  // 合并配置插件

const devConfig = {
    mode: 'development', // 开发模式
    //devtool: 'source-map', // 设置为none时无法定位语法错误发生在源代码里第几行
    devtool: 'cheap-module-eval-source-map',  // 开发模式最佳配置
    plugins: [
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
    ],
    module: {
        rules: [
            {
                test: /\.(css|scss)$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
        ]
    },
    devServer: {  // 配置devServer
        contentBase: './dist',  // 打包后起服务的目录名
        open: true,   // 自动打开浏览器
        proxy: {
            '/api': 'http://localhost:3000'  // 跨域代理转发
        },
        port: 8090,  // 指定端口号
        hot: true,  // 开启热模块替换
        //hotOnly: true  // 即便热模块替换没有生效,不让浏览器自动刷新
    },
    // 开发环境下,不用担心缓存的问题
    output: {
        filename: '[name].[hash].js', // 打包后的文件的名字,[name]是占位符,是上面的entry里的main或者sub
        chunkFilename: '[name].[hash].js',
    }
}

// 结合
module.exports = merge(commonConfig, devConfig);



// webpack.prod.js
const commonConfig = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const merge = require('webpack-merge'); // 合并配置插件
const prodConfig = {
    mode: 'production', // 生产模式
    devtool: 'cheap-module-source-map',  //生产模式最佳配置
    plugins: [
        // 分离css
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[id].css',
        })
    ],
    module: {
        rules: [
            {
                test: /\.(css|scss)$/,
                use: [
                    MiniCssExtractPlugin.loader,  //  把style-loader换成MiniCssExtractPlugin.loader
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
        ]
    },
    optimization: {
        //压缩CSS和js
        minimizer: [
            new TerserJSPlugin({}), //使用 UglifyJS 压缩混淆 js 代码
            new OptimizeCSSAssetsPlugin({})  // 压缩css
        ],
        // 将所有css合并到一个css里面
        splitChunks: {
            cacheGroups: {
                styles: {
                    name: 'styles',
                    test: /\.(css|scss)$/,
                    chunks: 'all',
                    enforce: true,
                }
            }
        }
    },
    // 生产环境下,需要这么写,因为缓存的问题
    output: {
        filename: '[name].[contenthash].js', // 打包后的文件的名字,[name]是占位符,是上面的entry里的main或者sub
        chunkFilename: '[name].[contenthash].js',
    }
}

// 结合
module.exports = merge(commonConfig, prodConfig);
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243

使用环境变量后,只需要修改webpack.common.js即可:

// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');
const merge = require('webpack-merge');
const devConfig = require('./webpack.dev.js');
const prodConfig = require('./webpack.prod.js');
const commonConfig = {
    //entry: './src/index.js', // 入口文件,等价于下面的main
    entry: {
        main: './src/index.js' // 第一个入口文件
    },
    performance: false,
    // 打包出口
    output: {
        //publicPath: '/',  // webpackDevServer要配置这个
        // publicPath: 'http://cdn.com/',  // 在打包后生成的js地址前面加上域名,即http://cdn.com/main.js
        path: path.resolve(__dirname, '../dist') // 打包后的目录,必须使用绝对路径,要用到path.resolve
    },
    // 配置相关loader
    module: {
        rules: [
            {
                test: /\.(jpg|png|jpeg)$/,
                use: {
                    // loader: 'file-loader',  // 使用 file-loader 打包
                    // options: {
                    //     name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                    //     outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                    // }
                    loader: 'url-loader', // 使用 url-loader 打包,在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
                    options: {
                        name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                        outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                        limit: 2048  // 图片小于2048字节就会打包成base64
                    }
                }
            },
            {
                test: /\.(eot|ttf|svg)$/,
                use: {
                    loader: 'file-loader'  // 使用 file-loader 打包
                }
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {
                    // 开发业务代码时的写法
                    //presets: ["@babel/preset-env"]
                    presets: [['@babel/preset-env', {
                        targets: {
                            edge: "17",
                            firefox: "60",
                            chrome: "67",
                            safari: "11.1",
                        },
                        useBuiltIns: 'usage',
                        corejs: "2", // 声明corejs版本
                    }]]
                    // 开发第三方模块时的写法
                    /*
                    "plugins": [
                        [
                            "@babel/plugin-transform-runtime",
                            {
                                "absoluteRuntime": false,
                                "corejs": 2,
                                "helpers": true,
                                "regenerator": true,
                                "useESModules": false
                            }
                        ]
                    ]
                    */
                }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin(),
        // 当任何一个模块中用了$,就自动加载juqery模块,并导入命名为$   
        new webpack.ProvidePlugin({
            $: 'jquery',
            _: 'lodash',
            _join: ['loadsh', 'join']
        })
    ],
    optimization: {
        //tree shaking 配置
        usedExports: true,  // 开发模式下需要配置,生产模式不需要,默认会tree shaking
        // Code Splitting配置
        splitChunks: {
            // chunks: 'all',
            // cacheGroups: {
            //     vendors: false,
            //     default: false
            // }
            chunks: "all",  // async,只对异步代码进行code spliting,all就是同步异步都分割
            minSize: 30000, // 代码分割最小的文件大小
            minChunks: 1, // 至少有1个chunk引用了这个模块,才会code Spliting
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
                // vendors
                vendors: {
                    test: /[\\/]node_modules[\\/]/,  // 代码必须在node_module里面才会分到这个vendors组
                    priority: -10,  // 打包的权重,会选择priority大的打包
                    //filename: "vendors.js"  // 仅对同步引入的模块生效,修改文件名
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true, // 如果一个模块被打包过了,则直接复用,不再做打包
                    //filename: "common.js" // 仅对同步引入的模块生效,修改文件名
                }
            }
        }
    }
}

module.exports = (env) => {
    // 如果是线上环境
    if (env && env.production) {
        return merge(commonConfig, prodConfig);
    }
    return merge(commonConfig, devConfig);
}





// webpack.dev.js
const webpack = require('webpack');

const devConfig = {
    mode: 'development', // 开发模式
    //devtool: 'source-map', // 设置为none时无法定位语法错误发生在源代码里第几行
    devtool: 'cheap-module-eval-source-map',  // 开发模式最佳配置
    plugins: [
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
    ],
    module: {
        rules: [
            {
                test: /\.(css|scss)$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
        ]
    },
    devServer: {  // 配置devServer
        contentBase: './dist',  // 打包后起服务的目录名
        open: true,   // 自动打开浏览器
        proxy: {
            '/api': 'http://localhost:3000'  // 跨域代理转发
        },
        port: 8090,  // 指定端口号
        hot: true,  // 开启热模块替换
        //hotOnly: true  // 即便热模块替换没有生效,不让浏览器自动刷新
    },
    // 开发环境下,不用担心缓存的问题
    output: {
        filename: '[name].[hash].js', // 打包后的文件的名字,[name]是占位符,是上面的entry里的main或者sub
        chunkFilename: '[name].[hash].js',
    }
}

// 结合
module.exports = devConfig





// webpack.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const prodConfig = {
    mode: 'production', // 生产模式
    devtool: 'cheap-module-source-map',  //生产模式最佳配置
    plugins: [
        // 分离css
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[id].css',
        })
    ],
    module: {
        rules: [
            {
                test: /\.(css|scss)$/,
                use: [
                    MiniCssExtractPlugin.loader,  //  把style-loader换成MiniCssExtractPlugin.loader
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            modules: true
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            },
        ]
    },
    optimization: {
        //压缩CSS和js
        minimizer: [
            new TerserJSPlugin({}), //使用 UglifyJS 压缩混淆 js 代码
            new OptimizeCSSAssetsPlugin({})  // 压缩css
        ],
        // 将所有css合并到一个css里面
        splitChunks: {
            cacheGroups: {
                styles: {
                    name: 'styles',
                    test: /\.(css|scss)$/,
                    chunks: 'all',
                    enforce: true,
                }
            }
        }
    },
    // 生产环境下,需要这么写,因为缓存的问题
    output: {
        filename: '[name].[contenthash].js', // 打包后的文件的名字,[name]是占位符,是上面的entry里的main或者sub
        chunkFilename: '[name].[contenthash].js',
    }
}

// 结合
module.exports = prodConfig;





// package.json
"scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.common.js",
    "prod": "webpack --env.production --config ./build/webpack.common.js"
 }
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264

# 十、Library的打包

// webpack.config.js
output: {
    libraryTarget:'umd',  // 支持常用的模块引入方式
    library:'library' // 支持script标签引入
},
externals:['lodash']  // 避免库所依赖的模块被多次打包


// package.json
"main": "./dist/index.js",   // 配置main为打包后的库文件的路径
1
2
3
4
5
6
7
8
9
10

如果我们需要使得打包出来的库文件支持多种模块引入方法(commonJS、ES Module、amd),就需要在output里面配置libraryTarget:'umd'参数。

如果需要将library挂载到window下面,则可以配置 library:'library' ,值是我们希望取的window下的名字,即全局变量的名字。

externals:['lodash']是避免库所依赖的模块被多次打包,只在业务里打包就行,不在库里打包。

# 十一、PWA的打包

Progressive Web App,即渐进式WEB应用。用户第一次访问某个服务器的web 页面,访问成功,当服务器挂了的时候,用户还能继续访问,但不是访问服务器而是访问本地缓存。这就叫做PWA。

安装workbox-webpack-plugin,npm i workbox-webpack-plugin -D

配置webpack:

// webpack.prod.js
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');

plugins: [
    // 分离css
    new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[id].css',
    }),
    // 应用这个插件
    new WorkboxWebpackPlugin.GenerateSW({
        clientsClaim: true,
        skipWaiting: true
    })
],

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

业务代码加入以下代码:

if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js').then(registration => {
            console.log('ServiceWorker registered');
        }).catch(err => {
            console.log('ServiceWorker registered err');
        })
    })
}
1
2
3
4
5
6
7
8
9

# 十二、TypeScript 的打包配置

安装 ts-loader和 typescript,npm i -D ts-loader typescript。

webpack配置:

 {
    test: /\.ts$/,
    exclude: /node_modules/,
    use: "ts-loader"
}
1
2
3
4
5

配置tsconfig.json

{
    "compilerOptions": {
        "module": "es6",
        "target": "es3",
        "allowJs": true,
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "sourceMap": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules",
        "**/*.spec.ts"
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

如果需要在ts文件里面对loadsh使用的语法进行自动检查,则还需要安装@types/loadsh。

更多tsconfig.json的配置

# 十三、使用 WebpackDevServer 实现请求转发

# 路径替换

接口地址:http://www.dell-lee.com/react/api/header.json

import axios from 'axios';

axios.get("/header.json")
.then((res)=>{
   console.log(res)
})
1
2
3
4
5
6

webpack配置:

devServer: {  // 配置devServer
    contentBase: './dist',  // 打包后起服务的目录名
    //open: true,   // 自动打开浏览器
    proxy: {
        '/header.json': 'http://www.dell-lee.com/react/api'  // 跨域代理转发,
    },
    port: 8090,  // 指定端口号
    hot: true,  // 开启热模块替换
    hotOnly: true  // 即便热模块替换没有生效,不让浏览器自动刷新
}
1
2
3
4
5
6
7
8
9
10
  • 配置方法:

当遇到/header.json时,则把 http://www.dell-lee.com/react/api/header.json 地址 /header.json 前面的部分,也就是 http://www.dell-lee.com/react/api 配置一下就可以。

所以以下配置也是可以的:

axios.get("/api/header.json")
.then((res)=>{
   console.log(res)
})
1
2
3
4

webpack配置:

devServer: {  // 配置devServer
    contentBase: './dist',  // 打包后起服务的目录名
    //open: true,   // 自动打开浏览器
    proxy: {
        '/api': 'http://www.dell-lee.com/react'  // 跨域代理转发
    },
    port: 8090,  // 指定端口号
    hot: true,  // 开启热模块替换
    hotOnly: true  // 即便热模块替换没有生效,不让浏览器自动刷新
},
1
2
3
4
5
6
7
8
9
10

# 路径重写

两个接口如下:

http://www.dell-lee.com/react/api/demo.json (测试数据)

http://www.dell-lee.com/react/api/header.json (正式数据)

axios.get("/api/header.json")
.then((res)=>{
   console.log(res.data)
})
1
2
3
4

使用以上代码,如果不使用路径重写,则会请求到正式数据,如果我们需要请求到测试数据,则需要使用路径重写。

 devServer: {  // 配置devServer
    contentBase: './dist',  // 打包后起服务的目录名
    //open: true,   // 自动打开浏览器
    proxy: {
        // '/api': 'http://www.dell-lee.com/react'  // 跨域代理转发
        // 下面进行路径重写
        '/api': {
            target: 'http://www.dell-lee.com/react',
            pathRewrite: {
                "header.json": "demo.json" // 把header.json换成demo.json
            }
        }
    },
    port: 8090,  // 指定端口号
    hot: true,  // 开启热模块替换
    hotOnly: true  // 即便热模块替换没有生效,不让浏览器自动刷新
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 支持https的转发

如果后台接口是https,则需要加下secure:false。

'/api': {
    target: 'https://www.dell-lee.com/react',
    secure:false,
    pathRewrite: {
        "header.json": "demo.json" // 把header.json换成demo.json
    }
}
1
2
3
4
5
6
7

# 支持转发跨域的请求

加 changeOrigin:true

 proxy: {
    // '/api': 'http://www.dell-lee.com/react'  // 跨域代理转发
    // 下面进行路径重写
    '/api': {
        target: 'http://www.dell-lee.com/react',
        pathRewrite: {
            "header.json": "demo.json" // 把header.json换成demo.json
        },
        changeOrigin:true   // 跨域,改变origin
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 十四、使用WebpackDevServer解决单页面应用路由问题

React代码,会发现当访问 http://localhost:8090/ 时,可以展示home页面,但是访问 http://localhost:8090/list 时,会404,。这是因为在服务端,默认会认为 http://localhost:8090/list 这个是访问 http://localhost:8090/list.html ,但实际上并没有list.html,所以就会报错。

import React, { Component } from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import Home from './home';
import List from './list';
class App extends Component {
   render() {
      return (
         <BrowserRouter>
            <div>
               <Route path='/' exact component={Home}></Route>
               <Route path='/list' component={List}></Route>
            </div>
         </BrowserRouter>
      )
   }
}

ReactDom.render(<App />, document.getElementById('root'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

解决方法是,修改devServer的配置

 devServer: {  
   historyApiFallback: true  // 解决单页面应用路由问题,等价于如果找不到页面,则返回index.html页面。即路由重写。
 }
1
2
3

# 十五、EsLint 在 Webpack 中的配置

安装eslint:npm i eslint -D

初始化eslint的配置文件:npx eslint --init

  • 使用eslint

1、npx eslint src 安装 npm i babel-eslint -D ,检查src目录下的文件,会发现一些语法没法用,比如jsx。需要使用npx eslint src ;来进一步解析代码是哪里不规范了。

2、用VS Code插件 用VS Code编辑器安装ESlint 插件,代码会自动标红。

  • 使用eslint-loader结合overlay实现在页面上提示代码不规范

安装 eslint-loader: npm i -D eslint-loader

webpack配置:

{
    test: /\.js$/,
    exclude: /node_modules/,
    use: ["babel-loader", {
        loader: "eslint-loader",
        options: {
            fix: true   // 自动修复一些不规范的地方
        }
    }]
}



devServer: {
     overlay:true  // 在页面上提示代码不规范
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 不需要遵循某些规范,可以这样配置
 // .eslintrc.js
rules:{
   'import/no-extraneous-dependencies':0,  // 不使用这个规范
   'react/prefer-stateless-function':0,
   'react/jsx-filename-extension':0
 },
1
2
3
4
5
6

eslint配置文件:

// .eslintrc.js
module.exports = {
 env: {
   browser: true,
   es6: true,
 },
 rules:{
   'import/no-extraneous-dependencies':0,  // 不使用这个规范
   'react/prefer-stateless-function':0,
   'react/jsx-filename-extension':0
 },
 parser: "babel-eslint",  // 用babel-eslint去检查
 extends: [
   'airbnb',
 ],
 globals: {
   Atomics: 'readonly',
   SharedArrayBuffer: 'readonly',
 },
 parserOptions: {
   ecmaFeatures: {
     jsx: true,
   },
   ecmaVersion: 2018,
   sourceType: 'module',
 },
 plugins: [
   'react',
 ]
};

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

# 十六、webpack性能优化

# 1、跟上技术的迭代(Node、NPM、Webpack)

# 2、在尽可能少的模块上应用loader (使用exclude或者include)

# 3、Plugin尽可能精简并确保可靠

# 4、resolve参数合理配置

   entry: {
        main: './src/index.js' // 第一个入口文件
    },
    resolve: {
        extensions: ['.js', '.jsx','.vue'], // 如果import引入的文件不带后缀名,则先找.js结尾的,再找.jsx结尾的,最后找.vue结尾的。 
        mainFiles:['index','child'],   // 如果import引入了一个目录,但没有写具体文件的名字,则先看有没有index名字的文件,如果没有,再看有没有child名字的文件
        alias: {
            '@src': path.resolve(__dirname, '../src/')
        }
    },
1
2
3
4
5
6
7
8
9
10
  • extensions 配置

extensions: ['.js', '.jsx','.vue'],

如果import引入的文件不带后缀名,则先找.js结尾的,再找.jsx结尾的,最后找.vue结尾的。

比如 import abc from './app/index'

  • mainFiles 配置

mainFiles:['index','child']

如果import引入了一个目录,但没有写具体文件的名字,则先看有没有index名字的文件,如果没有,再看有没有child名字的文件。

比如 import abc from './app/'

  • alias 配置
alias: {
    '@src': path.resolve(__dirname, '../src/')
}
1
2
3

alias就是起别名,当我们使用 import abc from '@src/list' 这样的语法时,alias会帮助我们自动将@src替换为 ../src/

# 5、使用DllPlugin提高打包速度

第三方模块代码不会变,不需要每次都打包。第一次打包的时候,可以作分析,下一次打包就根据分析,不作打包。这样就可以提高打包速度了。我们可以对第三方模块进行单独打包,生成dll文件。然后使用插件将dll文件注入开发模式或者生产模式打包后的html文件里面。

# 实现思路
  • 1、第三方模块单独打包,放在dll.js里面
  • 2、我们引入第三方模块的时候,要去使用dll文件里面的内容。可以使用DllPlugin插件,这样使用import引入第三方模块的时候,就不会去node_modules里面查找了,而是直接使用dll.js文件
# 具体实现步骤

1、新建一个webpack.dll.js

const path = require('path');
const webpack = require('webpack');
module.exports = {
    mode: 'production', // 生产模式
    entry: {
        vendors: ['react', 'react-dom'],   // 把三方库写在这里
    },
    output: {
        filename: '[name].dll.js',
        path: path.resolve(__dirname, '../dll'),
        library: '[name]'
    },
    plugins: [
        // 使用dllPlugin,生成库的分析文件
        new webpack.DllPlugin({
            name: '[name]',  // 对库文件进行分析,name和library一样
            path: path.resolve(__dirname, '../dll/[name].manifest.json'),
            // 把库和模块的映射关系放在manifest.json文件里面
        })
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

把库和模块的映射关系放在manifest.json文件里面。

2、 修改package.json

 "scripts": {
    "dll":"webpack  --config ./build/webpack.dll.js"
  }
1
2
3

运行npm run dll,这样在根目录下就会生成dll目录,并且包含vendors.dll.js文件和manifest.json文件。这样就对第三方模块单独打包了。

3、 使用插件将vendors.dll.js文件注入到开发模式或者生产模式打包后的html文件里面

安装插件:npm i add-asset-html-webpack-plugin -S

修改webpack.dev.js:

const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin'); // 引入这个插件

plugins: [
    // 把dll.js注入到html里面
    new addAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
    }),
    // 将映射关系注入到这里
    new webpack.DllReferencePlugin({
        manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
    })
]
1
2
3
4
5
6
7
8
9
10
11
12

使用 webpack.DllReferencePlugin 插件,这样当我们使用import引入第三方模块的时候,就不会去 node_modules 里面查找了,而是直接根据 vendors.manifest.json文件去vendors.dll.js找对应的第三方模块,避免从 node_module里面找。

# 多个dll和manifest.json的情况

需要动态读取dll目录里面的内容

// webpack.dev.js
const webpack = require('webpack');
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const fs = require('fs');

// 常用的插件
const plugins = [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    }),
    new CleanWebpackPlugin(),
    // 当任何一个模块中用了$,就自动加载juqery模块,并导入命名为$   
    new webpack.ProvidePlugin({
        $: 'jquery',
        _: 'lodash',
        _join: ['loadsh', 'join']
    }),
]
// 动态读取dll目录里面的内容
const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.map(file => {
    if (/.*\.dll\.js/.test(file)) {
        plugins.push(new addAssetHtmlWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll', file)
        }))
    }
    if (/.*\.manifest\.json/.test(file)) {
        plugins.push(new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '../dll', file)
        }))
    }
});

module.export={
    plugins: plugins  // plugins
}


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

# 6、控制包文件大小

# 7、thread-loader,parallel-webpack,happypack多进程打包

# 8、合理使用sourceMap

# 9、结合stats分析打包结果

# 10、开发环境内存编译

# 11、开发环境无用插件剔除

# 十七、多页面打包配置

一个entry入口对应一个html文件

// webpack.dev.js
entry: {
    index: './src/index.js', // 第一个入口文件
    list:'./src/list.js'  // 第二个入口文件
},


const plugins = [
    // 多页面打包配置
    // 第一个页面
    new HtmlWebpackPlugin({
        template: 'src/index.html',
        filename: 'index.html', // 取名为index.html
        chunks: ['vendors', 'index']  // 配置需要的chunk
    }),
    // 第二个页面
    new HtmlWebpackPlugin({
        template: 'src/index.html',
        filename: 'list.html', // 取名为list.html
        chunks: ['vendors', 'list']  // 配置需要的chunk
    }),
    new CleanWebpackPlugin(),
    // 当任何一个模块中用了$,就自动加载juqery模块,并导入命名为$   
    new webpack.ProvidePlugin({
        $: 'jquery',
        _: 'lodash',
        _join: ['loadsh', 'join']
    }),
]
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

如果需要动态读取entry的内容添加多个HtmlWebpackPlugin,则作以下配置,注意需要安装html-webpack-plugin@next,因为html-webpack-plugin不会将公用的vendors打包到两个页面里面。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');
const merge = require('webpack-merge');
const devConfig = require('./webpack.dev.js');
const prodConfig = require('./webpack.prod.js');
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const fs = require('fs');

// 常用的插件
const makePlugins = (config) => {
    // 基础plugins
    const plugins = [
        new CleanWebpackPlugin(),
        // 当任何一个模块中用了$,就自动加载juqery模块,并导入命名为$   
        new webpack.ProvidePlugin({
            $: 'jquery',
            _: 'lodash',
            _join: ['loadsh', 'join']
        })
    ];

    // 动态读取entry来自动添加htmlWebpackPlugins
    Object.keys(config.entry).map(item => {
        plugins.push(
            new HtmlWebpackPlugin({
                template: 'src/index.html',
                filename: item + '.html',
                chunks: ['vendors', item]  
    // 配置需要的chunk,注意需要安装html-webpack-plugin@next,不然vendors不会打包到页面里
            })
        );
    });

    // 动态读取dll目录里面的内容,添加addAssetHtmlWebpackPlugin和DllReferencePlugin
    const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
    files.map(file => {
        if (/.*\.dll\.js/.test(file)) {
            plugins.push(new addAssetHtmlWebpackPlugin({
                filepath: path.resolve(__dirname, '../dll', file)
            }))
        }
        if (/.*\.manifest\.json/.test(file)) {
            plugins.push(new webpack.DllReferencePlugin({
                manifest: path.resolve(__dirname, '../dll', file)
            }))
        }
    });
    return plugins;
}


const commonConfig = {
    //entry: './src/index.js', // 入口文件,等价于下面的main
    entry: {
        index: './src/index.js', // 第一个入口文件
        list: './src/list.js'  // 第二个入口文件
    },
    resolve: {
        extensions: ['.js', '.jsx'],
        mainFiles: ['index'],
        alias: {
            '@src': path.resolve(__dirname, '../src/')
        }
    },
    performance: false,
    // 打包出口
    output: {
        libraryTarget: 'umd',
        //publicPath: '/',  // webpackDevServer要配置这个
        // publicPath: 'http://cdn.com/',  // 在打包后生成的js地址前面加上域名,即http://cdn.com/main.js
        path: path.resolve(__dirname, '../dist') // 打包后的目录,必须使用绝对路径,要用到path.resolve
    },
    // 配置相关loader
    module: {
        rules: [
            {
                test: /\.(jpg|png|jpeg)$/,
                use: {
                    // loader: 'file-loader',  // 使用 file-loader 打包
                    // options: {
                    //     name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                    //     outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                    // }
                    loader: 'url-loader', // 使用 url-loader 打包,在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
                    options: {
                        name: '[name].[ext]',// 配置打包后图片的名字,[name]就是图片的原名字,[ext]是原后缀
                        outputPath: 'images/',  // 将打包后的图片放在images目录下,不配置则会放在根目录下
                        limit: 2048  // 图片小于2048字节就会打包成base64
                    }
                }
            },
            {
                test: /\.(eot|ttf|svg)$/,
                use: {
                    loader: 'file-loader'  // 使用 file-loader 打包
                }
            },
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                // use: ["babel-loader", {
                //     loader: "eslint-loader",
                //     options: {
                //         fix: true
                //     }
                // }]
                options: {
                    // 开发业务代码时的写法
                    //presets: ["@babel/preset-env"]
                    presets: [['@babel/preset-env', {
                        targets: {
                            edge: "17",
                            firefox: "60",
                            chrome: "67",
                            safari: "11.1",
                        },
                        useBuiltIns: 'usage',
                        corejs: "2", // 声明corejs版本
                    }]]
                    // 开发第三方模块时的写法
                    /*
                    "plugins": [
                        [
                            "@babel/plugin-transform-runtime",
                            {
                                "absoluteRuntime": false,
                                "corejs": 2,
                                "helpers": true,
                                "regenerator": true,
                                "useESModules": false
                            }
                        ]
                    ]
                    */
                }
            },
            {
                test: /\.ts$/,
                exclude: /node_modules/,
                use: "ts-loader"
            }
        ]
    },
    optimization: {
        //tree shaking 配置
        usedExports: true,  // 开发模式下需要配置,生产模式不需要,默认会tree shaking
        // Code Splitting配置
        splitChunks: {
            // chunks: 'all',
            // cacheGroups: {
            //     vendors: false,
            //     default: false
            // }
            chunks: "all",  // async,只对异步代码进行code spliting,all就是同步异步都分割
            minSize: 30000, // 代码分割最小的文件大小
            minChunks: 1, // 至少有1个chunk引用了这个模块,才会code Spliting
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
                // vendors
                vendors: {
                    test: /[\\/]node_modules[\\/]/,  // 代码必须在node_module里面才会分到这个vendors组
                    priority: -10,  // 打包的权重,会选择priority大的打包
                    //filename: "vendors.js"  // 仅对同步引入的模块生效,修改文件名
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true, // 如果一个模块被打包过了,则直接复用,不再做打包
                    //filename: "common.js" // 仅对同步引入的模块生效,修改文件名
                }
            }
        }
    }
}
commonConfig.plugins = makePlugins(commonConfig);

module.exports = (env) => {
    // 如果是线上环境
    if (env && env.production) {
        return merge(commonConfig, prodConfig);
    }
    return merge(commonConfig, devConfig);
}
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189

# 十八、打包第三方库

通过配置 CommonsChunkPlugin来打包第三方库。

entry: {
    index: './app/main.jsx',
    vendor: ['react', 'react-dom', 'react-router', 'classnames']
},
output: {
    path: path.resolve(__dirname, './dist'),
    filename: "[name].[chunkHash:8].js",
    publicPath: '',
    chunkFilename: "[name].[chunkHash:8].js",
},
plugins: [
    new webpack.optimize.CommonsChunkPlugin({
        names: ['vendor', 'manifest'],
    }),
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Last Updated: 5/2/2020, 2:51:33 PM