Webpack学习笔记

一、Webpack

Webpack 是一种前端资源构建工具,一个静态模块打包器(module bundler)

静态模块打包

在 Webpack 中前端所有资源文件(js/json/css/img/less/…)都会作为模块处理,它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源(bundle)

1、Webpack 五个核心概念

(1)Entry

入口(Entry)指示 Webpack 以哪个文件为入口起点开始打包,分析构建内部依赖图

(2)Output

输出(Output)指示 Webpack 打包后的资源 bundles 输出到哪里,以及如何命名

(3)Loader

Loader 让 Webpack 能够处理那些非 JavaScript 文件(如样式 css 文件、图片等)(Webpack 自身只能理解 JavaScript)

使用 loader 需要:1)下载 2)使用(配置loader)

(4)Plugins

插件(Plugins)可以用于执行范围更广的任务,插件的范围包括从打包优化和压缩,到重新定义环境中的变量等

使用 plugins 需要:1)下载 2)引入 3)使用

(5)Mode

模式(Mode)指示 Webpack 使用相应模式的配置,模式分为 development 开发模式和 production 生产模式

development

development 会将 process.env.NODE_ENV 的值设置为 development,启用 NamedChunksPlugin 和 NamesModulesPlugin

特点:能让代码本地调试运行的环境

production

production 会将 process.env.NODE_ENV 的值设置为 production,启用 FlagDependencyUsagePlugin、FlagIncludedChunksPlugin、ModuleConcatenationPlugin、NoEmitOnErrorsPlugin、OccurenceOrderPlugin、SideEffectsFlagPlugin、UglifyJsPlugin/TerserPlugin(压缩 js 代码)

特点:能让代码优化上线运行的环境

开发环境和生产环境的异同

生产环境和开发环境能将 ES6 模块化编译成浏览器能识别的模块化

生产环境生成的打包文件压缩了 js 代码,而开发环境没有,因此生产环境打包后生成的文件较小

2、使用 Webpack

新建项目文件夹,在文件夹下使用命令 npm init 初始化生成 package.json 文件

全局安装 webpack 和 webpack-cli npm i webpack webpack-cli -g

本地安装 webpack 和 webpack-cli npm i webpack webpack-cli -D

在项目下新建文件夹 src 存放源码,并新建 index.js 文件作为入口文件,在项目下新建文件夹 build 存放打包后的文件

在 index.js 中编写相关代码

function add(x,y){
    return x+y;
}
console.log(add(1,2))

运行指令

开发环境中
webpack ./src/index.js -o ./build/built.js --mode=development
webpack 会以 ./src/index.js 为入口文件开始打包,打包后输出到 ./build/built.js,整体打包环境是开发环境

生产环境
webpack ./src/index.js -o ./build/built.js --mode=production
webpack 会以 ./src/index.js 为入口文件开始打包,打包后输出到 ./build/built.js,整体打包环境是生产环境

打包后会生成 built.js 文件,且命令行输出中有个 Hash 相当于文件的 id,后面可以利用它作为文件命名的方式

在生产环境生成的 built.js 中压缩了 js 代码

使用 node .\build\built.js 运行代码,也可在 html 文件中使用 <script> 标签引入打包后的 built.js 文件

3、使用 webpack 打包样式资源

要打包样式资源需要用到 loader,而 loader 需要定义配置文件

在和 src 同级目录下新建配置文件 webpack.config.js,运行 webpack 指令时会加载里面的配置指示 webpack 干哪些活,内容如下

//resolve 用来拼接绝对路径的方法
const {resolve} = require('path');
module.exports = {
    //webpack 配置
    //入口起点
    entry:'./src/index.js',
    //输出
    output:{
        //输出文件名
        filename: 'built.js',
        //输出路径,__dirname 是 node.js 的变量,代表当前文件的目录绝对路径
        path: resove(__dirname,'build')
    },
    //loader 的配置
    module:{
        rules:[
            //详细 loader 配置,不同类型文件必须配置不同 loader 处理
            //处理 css 文件
            {
                //匹配哪些文件
                test:/\.css$/, //匹配以 .css 结尾的文件
                //使用哪些loader进行处理
                use:[
                    //use 数组中 loader 执行顺序:从右到左,从下到上依次执行,下列 loader 先执行 css-loader 再执行 style-loader
                    'style-loader',  //创建 style 标签,将 js 中的样式资源插入进去,添加到 head 中生效
                    'css-loader'  //将 css 文件变成 CommonJS 模块加载到 js 中,里面内容是样式字符串
                ]
            },
            //处理 less 文件
            {
                test:/\.less$/, //匹配以 .less 结尾的文件
                use:[
                    'style-loader',  //创建 style 标签,将 js 中的样式资源插入进去,添加到 head 中生效
                    'css-loader',  //将 css 文件变成 CommonJS 模块加载到 js 中,里面内容是样式字符串
                    'less-loader'  //将 less 文件编译成 css 文件,注意这里需要下载 less-loader 和 less
                ]
            }
        ]
    },
    //plugins 的配置
    plugins:[
        //详细 plugins 的配置
    ],
    //模式
    mode: 'development',
    //mode:'production'
}

注意:要使用多个 loader 处理时用 use,若只需一个 loader 时再 rules 的对象中直接使用 loader: 进行配置

注意:所有构建工具都是基于 node.js 平台运行的,模块化默认采用 CommonJS 规范,而 src 文件夹中写的项目相关代码所使用的模块化是 ES6 规范的

4、使用 webpack 打包 html 资源

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
        ]
    },
    //plugins 的配置
    plugins:[
        //详细 plugins 的配置
        //html-webpack-plugin 默认会创建一个空的 html 文件,自动引入打包输出的所有资源(js/css)
        new HtmlWebpackPlugin({
            template:'./src/index.html' //复制 ./src/index.html 文件并自动引入打包输出的所有资源
        })
    ],
    //模式
    mode: 'development'
}

5、使用 webpack 打包图片资源

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            {
                test:/\.less$/,
                use:['style-loader','css-loader','less-loader']
            },
            //处理图片资源
            {
                test:/\.(jpg|png|gif)$/,
                loader:'url-loader', //需要下载 url-loader 和 file-loader 两个包
                options:{
                    //这里设置图片大小小于 8kb 就会被转换为 base64 编码的一种字符串,以 base64 处理
                    //优点:减少请求数量,减轻服务器压力
                    //缺点:图片体积会更大,文件请求速度更慢(如原先图片 10kb,转为 base64 编码后可能有 14kb)
                    //所以一般不会对大图片进行 base64 处理,会对 8-12kb 图片进行 base64 处理
                    limit:8*1024,
                    esModule: false,
                    //给图片重命名
                    name:'[hash:10].[ext]'//取图片 hash 的前 10 位以及文件原扩展名来命名,这样文件也会小一点
                }
            },
            //注意:上面的方式默认处理不了 html 中的 img 图片(通过 img 标签的 src 属性引入的图片)
            {
                test:/\.html$/,
                //处理 html 文件的 img 图片(负责引入 img,从而能被 url-loader 进行处理)
                loader:'html-loader'
            }
        ]
    },
    //plugins 的配置
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        })
    ],
    //模式
    mode: 'development'
}

注意:打包图片资源需要下载 url-loader 和 file-loader 两个包,因为 url-loader 依赖于 file-loader

问题:url-loader 默认处理不了 html 中的 img 图片,因为 url-loader 是使用 ES6 中的 module 去处理模块,而 html-loader 打包后引入的 img 是通过 CommonJS 规范引入,所以通过 url-loader 去解析 html-loader 引入的图片解析不了会报错,即打包后 img 标签中 src 会变成 [object Module]

解决:esModule: false 关闭 url-loader 的 ES6 模块化,使用 CommonJS 解析

注意:当一个文件多次使用时,webpack 只会打包一次,不会重复打包一个文件

6、使用 webpack 打包其他资源(如字体图标)

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            {
                test:/\.css$/,
                use:['style-loader','css-loader']
            },
            //打包其他资源(除了 html/js/css/less 以外的资源)
            {
                //排除 css/js/html 资源
                exclude:/\.(css|js|html|less)$/,
                loader:'file-loader'
            },
            options:{
                name:'[hash:10].[ext]'
            }
        ]
    },
    //plugins 的配置
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        })
    ],
    //模式
    mode: 'development'
}

7、devServer

开发服务器 devServer 用来自动化,可自动编译、自动打开浏览器、自动刷新浏览器

特点:只会在内存中编译打包,不会有任何输出

需下载包 webpack-dev-server,启动 devServer 指令:npx webpack-dev-server(若没有全局安装 webpack-dev-server 需要使用 npx 启动)

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            {
                test:/\.css$/,
                use:['style-loader','css-loader']
            },
            //打包其他资源(除了 html/js/css/less 以外的资源)
            {
                //排除 css/js/html 资源
                exclude:/\.(css|js|html|less)$/,
                loader:'file-loader'
            },
            options:{
                name:'[hash:10].[ext]'
            }
        ]
    },
    //plugins 的配置
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        })
    ],
    //模式
    mode: 'development',
    devServer:{
        contentBase:resolve(__dirname,'build'), //要运行的项目构建后的目录
        compress:true,  //启动 gzip 压缩,使代码更小运行更快
        port:3000,  //端口号
        open:true  //自动打开浏览器
    }
}

8、开发环境基本配置

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'js/built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            //处理 css
            {
                test:/\.less$/,
                use:['style-loader','css-loader','less-loader']
            },
            //处理 less
            {
                test:/\.css$/,
                use:['style-loader','css-loader']
            },
            //处理图片资源
            {
                test:/\.(jpg|png|gif)$/,
                loader:'url-loader',
                options:{
                    limit:8*1024,
                    esModule: false,
                    name:'[hash:10].[ext]',
                    outputPath:'imgs'  //打包后图片会输出到打包目录下的 imgs 文件夹中
                }
            },
            //处理 html 中 img 资源
            {
                test:/\.html$/,
                loader:'html-loader'
            },
            //处理其他资源
            {
                exclude:/\.(css|js|html|less|jpg|png|gif)/,
                loader:'file-loader',
                options:{
                    name:'[hash:10].[ext]',
                    outputPath:'media'  //打包后图片会输出到打包目录下的 media 文件夹中
                }
            }
        ]
    },
    //plugins 的配置
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        })
    ],
    devServer:{
        contentBase:resolve(__dirname,'build'), 
        compress:true,
        port:3000,
        open:true
    },
    //模式
    mode: 'development'
}

运行项目指令:

1)webpack 会将打包结果输出出去

2)npx webpack-dev-server 只会在内存中编译打包,没有输出

9、生产环境

生产环境中需解决的问题

(1)经过上面的配置,css 文件在打包后都会添加到 js 代码中,使得 js 代码很大,且由于要先加载 js 代码才能解析出其中所需的 css 样式,因此会出现闪屏现象

(2)在生产环境中需要对代码进行压缩

(3)还需要考虑一些样式代码(如一些 css3 的效果)和 js 的兼容性问题

……

提取 css 成单独文件

使用插件 mini-css-extract-plugin 可将 css 提取成单独文件

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'js/built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            {
                test:/\.css$/,
                use:[
                    //'style-loader', //创建 style 标签,将样式放入
                    MiniCssExtractPlugin.loader, //这个 loader 取代 style-loader,提取 js 中的 css 成单独文件
                    'css-loader' //将 css 文件整合到 js 文件中
                ]
            }
        ]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        }),
        new MiniCssExtractPlugin({
            filename: 'css/built.css' //对输出的 css 文件进行重命名,生成的 css 文件会在 build/css/built.css
        })
    ],
    mode: 'development'
}

通过上述方式从打包生成的 js 文件中提取 css 样式成单独文件后,生成的 build/index.html 中引入样式的方式就不是通过 style 标签引入,而是通过 <link ref="css/built.css" rel="stylesheet"> 引入样式,这样也解决了闪屏现象,并且生成的 js 文件体积也没那么大了

css 兼容性处理

css 兼容性处理需要使用库 postcss,在 webpack 中通过 postcss-loader 以及插件 postcss-preset-env(帮助 postcss 识别某些环境,从而加载指定的配置,能让兼容性精确到某一个浏览器的版本)使用

其中 postcss-preset-env 帮助 postcss 找到 package.json 中 browserlist 里面的配置,通过配置加载指定的 css 兼容性样式

安装 postcss-loader 和 postcss-preset-env npm i postcss-loader postcss-preset-env -D

在 webpack.config.js 中内容如下

//设置 Nodejs 环境变量,决定使用 browserlists 的哪个环境,因为默认是看 package.json 中的生产环境
process.env.NODE_ENV = 'development';
const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'js/built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            {
                test:/\.css$/,
                use:[
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    //使用 loader 的默认配置
                    //'postcss-loader' //相当于{loader:'postcss-loader'}
                    //修改 loader 的配置
                    {
                        loader:'postcss-loader',
                        options:{
                            ident: 'postcss',
                            plugins: () => [
                                //postcss 的插件
                                require('postcss-preset-env')()
                            ]
                        }
                    }
                ]
            }
        ]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        }),
        new MiniCssExtractPlugin({
            filename: 'css/built.css'
        })
    ],
    mode: 'development'
}

在 package.json 文件中添加如下内容,在开发环境中兼容最新版本的各浏览器,在生产环境中兼容 99.8% 的浏览器,不要已经 dead 和 op_mini 浏览器

"browserslist": {
    "development": [
        "last 1 chrome version",
        "last 1 firefox version",
        "last 1 safari version"
    ]
    "production": [
        ">0.2%",
        "not dead",
        "not op_mini all"
    ]
}

压缩 css

使用插件 optimize-css-assets-webpack-plugin 进行压缩

安装 npm i optimize-css-assets-webpack-plugin -D

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'js/built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            {
                test:/\.css$/,
                use:[
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    {
                        loader:'postcss-loader',
                        options:{
                            ident: 'postcss',
                            plugins: () => [
                                //postcss 的插件
                                require('postcss-preset-env')()
                            ]
                        }
                    }
                ]
            }
        ]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        }),
        new MiniCssExtractPlugin({
            filename: 'css/built.css'
        }),
        //压缩 css
        new OptimizeCssAssetsWebpackPlugin()
    ],
    mode: 'development'
}

js 语法检查 eslint

在 webpack 中通过 eslint-loader 进行语法检查,eslint-loader 依赖于 eslint 库,因此需要安装两者,npm i eslint-loader eslint -D

注意:语法检查只检查自己写的源代码,第三方的库是不用检查的

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'js/built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            {
                test:/\.js$/,
                exclude: /node_modules/, //不检查 node_modules 中的第三方库
                loader:'eslint-loader',
                options:{
                    fix:true //自动修复 eslint 的错误
                }
            }
        ]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        })
    ],
    mode: 'development'
}

这里使用airbnb 代码规范对 eslint 进行配置,还需要下载库 eslint-config-airbnb-base 和 eslint-plugin-import

在 package.json 中的 eslintConfig 中设置

“eslintConfig”:{
    "extends":"airbnb-base"
}

项目上线时无需 console.log 语句,可在源代码中的 console.log 语句的上一行加上 //eslint-disable-next-line 使得下一行 eslint 所有规则都失效,即不对下一行进行 eslint 检查

js 兼容性处理 eslint

在 IE 浏览器中不能识别 ES6 语法,因此需要对 js 进行兼容性处理,将 ES6 语法转换为 ES5 及以下的语法

在 webpack 中通过使用 babel-loader,同时还需要下载 @babel/core 和 @babel/preset-env

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'js/built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            {
                test:/\.js$/,
                exclude: /node_modules/, //不检查 node_modules 中的第三方库
                loader:'babel-loader',
                options:{
                    presets:['@babel/preset-env'] //预设:指示 babel 做怎样的兼容性处理
                }
            }
        ],
        plugins:[
            new HtmlWebpackPlugin({
                template:'./src/index.html'
            })
        ],
        mode: 'development'
    }
}

其中在 babel-loader 的配置 presets 中

1、@babel/preset-env 
    实现基本 js 兼容性处理
    问题:只能转换基本语法,如 promise 等高级不能转换
2、@babel/polyfill 
    可对全部 js 做兼容性处理,注意它不是作为插件在配置中设置,只需在 js 源代码中使用 import '@babel/polyfill' 引入即可
    问题:往往只需解决部分兼容性问题,但它会将所有兼容性代码全部引入,使得代码文件体积太大了
3、core-js
    按需加载,需要做兼容性处理的就做,同时 presets 设置需改为
    presets:[
        [
            '@babel/preset-env',
            {
                //按需加载
                useBuiltIns: 'usage',
                //指定 core-js 版本
                corejs: {
                    version: 3
                },
                //指定兼容性做到哪个版本浏览器
                targets: {
                    chrome: '60',
                    firefox: '60',
                    ie: '9',
                    safari: '10',
                    edge: '17'
                }
            }
        ]
    ]

通过 1、3 或 1、2(一般不用) 搭配使用

压缩 html 和 js

要完成 js 的压缩只需将 webpack.config.js 文件中的 mode 设置为 production 即可,因为生产环境下会自动压缩 js 代码

要完成 html 代码的压缩只需在 webpack.config.js 文件中的 plugin 中进行设置

plugins:[
    new HtmlWebpackPlugin({
        template:'./src/index.html',
        //压缩 html 代码
        minify: {
            //移除空格
            collapseWhitespace: true,
            //移除注释
            removeComments: true
        }
    })
]

注意:html 代码无需做兼容性处理,浏览器认识就认识,不认识就不认识

生产环境基本配置

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
//复用 loader
const commonCssLoader = [
    MiniCssExtractPlugin.loader,
    'css-loader',
    {
        //还需要在 package.json 中定义 browserslist
        loader:'postcss-loader',
        options:{
            ident: 'postcss',
            plugins: () => [
                require('postcss-preset-env')()
            ]
        }
    }
]
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'js/built.js',
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            //处理 css
            {
                test:/\.less$/,
                use:[...commonCssLoader]
            },
            //处理 less
            {
                test:/\.css$/,
                use:[...commonCssLoader,'less-loader']
            },
            //js 语法检查
            {
                //还需在 package.json 中配置 eslintConfig
                test:/\.js$/,
                exclude: /node_modules/,
                enforce: 'pre', //优先执行
                loader:'eslint-loader',
                options:{
                    fix:true
                }
            },
            // js 兼容性处理
            {
                test:/\.js$/,
                exclude: /node_modules/,
                loader:'babel-loader',
                options:{
                    presets:[
                        [
                            '@babel/preset-env',
                            {
                                useBuiltIns: 'usage',
                                corejs: {
                                    version: 3
                                },
                                targets: {
                                    chrome: '60',
                                    firefox: '60',
                                    ie: '9',
                                    safari: '10',
                                    edge: '17'
                                }
                            }
                        ]
                    ]
                }
            },
            //处理图片资源
            {
                test:/\.(jpg|png|gif)$/,
                loader:'url-loader',
                options:{
                    limit:8*1024,
                    esModule: false,
                    name:'[hash:10].[ext]',
                    outputPath:'imgs'  //打包后图片会输出到打包目录下的 imgs 文件夹中
                }
            },
            //处理 html 中 img 资源
            {
                test:/\.html$/,
                loader:'html-loader'
            },
            //处理其他资源
            {
                exclude:/\.(css|js|html|less|jpg|png|gif)$/,
                loader:'file-loader',
                options:{
                    name:'[hash:10].[ext]',
                    outputPath:'media'  //打包后图片会输出到打包目录下的 media 文件夹中
                }
            }
        ]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            minify: {
                collapseWhitespace: true,
                removeComments: true
            }
        });
        new MiniCssExtractPlugin({
            filename: 'css/built.css'
        }),
        //压缩 css
        new OptimizeCssAssetsWebpackPlugin()
    ],
    //模式
    mode: 'production'
}

还需在 package.json 文件中添加如下内容

"browserslist": {
    "development": [
        "last 1 chrome version",
        "last 1 firefox version",
        "last 1 safari version"
    ]
    "production": [
        ">0.2%",
        "not dead",
        "not op_mini all"
    ]
},
“eslintConfig”:{
    "extends":"airbnb-base"
}

注意:正常来讲一个文件只能被一个 loader 处理,当一个文件要被多个 loader 处理时,一定要指定 loader 的执行现后顺序,如 js 的语法检查和兼容性处理需要先执行 eslint 再执行 babel

原因1:先做语法检查,若语法检查出现错误后续工作无意义

原因2:使用 babel 后会把 ES6 语法转为 ES5 及以下语法,若此时再使用 eslint 进行语法检查又会报错,如 var 等不好用

10、性能优化

webpack 的性能优化包括开发环境性能优化和生产环境性能优化

开发环境性能优化

开发环境性能优化包括(1)优化打包构建速度(通过 HMR)(2)优化代码调试(通过 source-map)

(1)优化打包构建速度 ———— HMR

问题1:在开发环境中若只修改 css 样式文件或只修改了某个文件,其他文件没有变动,但 webpack 会把 css、js 等文件一起全部重新打包一次

解决:可通过 webpack 的 HMR(hot module replacement 热模块替换/模块热替换)功能,HMR 是基于 devServer 的

作用:一个模块发生变化,只会重新打包这个模块,而不是打包所有模块,极大提升了构建速度

只需修改 webpack.config.js 文件中的 devServer 设置,添加语句 hot: true 开启 HMR 功能

devServer:{
    contentBase: resolve(__dirname,'build'), 
    compress: true,
    port: 3000,
    open: true,
    //开启 HMR 功能
    hot: true
},

启动 webpack 服务:npx webpack-dev-server

问题2:

样式文件:可以使用 HMR 功能,因为 style-loader 内部实现了

js 文件:默认不能使用 HMR 功能

html 文件:默认不能使用 HMR 功能,同时会导致 html 文件不能热更新了

解决:修改 webpack.config.js 文件中的 entry 入口,将 html 文件加入,此时若修改 html 文件能热更新了,整个页面重新刷新,依然不能使用 HMR 功能(但是 html 无需 HMR 功能,因为项目中只会创建一个 html 文件,当该文件发生变化时自然也需要重新加载这个文件)

entry:['./src/js/index.js','./src/index.html']

在 index.js 文件中添加如下代码让 js 文件的 HMR 功能生效

if(module.hot){
    //一旦 module.hot 为 true,说明开启了 HMR 功能
    module.hot.accept(./xxx1.js,function(){
        //方法会监听 xxx.js 文件的变化,一旦发生变化,其他模块不会重新打包构建,会执行回调函数
        print();
    });
    module.hot.accept(./xxx2.js,function(){
        print();
    })
}

此时若修改 xxx1.js 文件或 xxx2.js 文件只会重新加载相应文件

注意:HMR 功能对 js 的处理,只能处理非入口 js 文件的其他文件

注意:当修改了 webpack 配置,新配置要想生效,必须重新启动 webpack 服务

(2)优化代码调试 ———— source-map

source-map 是一种提供源代码到构建后代码映射的技术,若构建后代码出错了,会通过映射关系可以追踪源代码错误

在 webpack.config.js 文件中添加

devtool: 'source-map'

打包后会在 js 的输出文件夹下生成 built.js.map 文件

devtool 的参数有[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map,其中

source-map:外部,能提供错误代码的准确信息和源代码的错误位置(第几行)
inline-source-map:内联,只生成一个 source-map,能提供错误代码的准确信息和源代码的错误位置
hidden-source-map:外部,能提供代码错误的原因,不能追踪源代码错误,只能提示构建后代码的错误位置(可以半隐藏源代码)
eval-source-map:内联,每个文件都生成对应的 source-map,都在 eval 中,能提供错误代码的准确信息和源代码的错误位置(第几行),只是在错误文件名中多了个哈希值
nosources-source-map:外部,能提供错误代码的准确信息,但没有任何源代码信息(可以隐藏源代码)
cheap-source-map:外部,能提供错误代码的准确信息和源代码的错误位置,但只能精确到行,而前面的方式(如 source-map 可精确到行列)
cheap-module-source:外部,能提供错误代码的准确信息和源代码的错误位置,但只能精确到行,module 会将 loader 的 source-map 加入

外部方式和内联方式的区别:(1)使用外部方式会生成 built.js.map 文件,而使用内联方式不会生成文件,而是嵌入在 built.js 文件中,(2)但内联的构建速度更快

开发环境下:需要速度快,调试更友好

速度快(eval > inline > cheap > ...)
    最快:eval-cheap-source-map
    其次:eval-source-map
调试更友好
    最友好:source-map
    其次:cheap-module-source-map
    再其次:cheap-source-map
综合:可选择 eval-source-map 或 eval-cheap-module-source-map

生产环境下:需要考虑源代码要不要隐藏?调试要不要更友好?

由于内联会让代码体积变大,所以在生产环境不用内联

考虑源代码隐藏
    nosources-source-map(全部隐藏)
    hidden-source-map(只隐藏源代码,会提示构建后代码的错误信息和位置)
调试更友好
    source-map
    同时速度更快点:cheap-module-source-map

生产环境性能优化

生产环境性能优化包括(1)优化打包构建速度(oneOf、babel缓存、多进程打包、externals、DLL)(2)优化代码运行的性能(文件资源缓存、tree shaking、code split、懒加载/预加载、PWA)

(1)优化打包构建速度

问题:一般一个文件只会使用一种 loader 进行处理(除了 js 文件需要 eslint-loader 和 babel-loader 两种进行处理),但是每加载一个文件都会把所有 loader 过一遍,影响速度

oneOf

解决1:通过将 webpack.config.js 中的各种 loader 放在 oneOf 中,并把 eslint-loader 单独放到 oneOf 前面

module:{
    rules:[
        //js 语法检查
        {
            //还需在 package.json 中配置 eslintConfig
            test:/\.js$/,
            exclude: /node_modules/,
            enforce: 'pre', //优先执行
            loader:'eslint-loader',
            options:{
                fix:true
            }
        },
        {
            //以下 loader 只会匹配一个,不能有两个配置处理同一种类型文件
            oneOf:[
                //处理 css
                {
                    test:/\.less$/,
                    use:[...commonCssLoader]
                },
                //处理 less
                {
                    test:/\.css$/,
                    use:[...commonCssLoader,'less-loader']
                },
                // js 兼容性处理
                {
                    test:/\.js$/,
                    exclude: /node_modules/,
                    loader:'babel-loader',
                    options:{
                        presets:[
                            [
                                '@babel/preset-env',
                                {
                                    useBuiltIns: 'usage',
                                    corejs: {
                                        version: 3
                                    },
                                    targets: {
                                        chrome: '60',
                                        firefox: '60',
                                        ie: '9',
                                        safari: '10',
                                        edge: '17'
                                    }
                                }
                            ]
                        ]
                    }
                },
                //处理图片资源
                {
                    test:/\.(jpg|png|gif)$/,
                    loader:'url-loader',
                    options:{
                        limit:8*1024,
                        esModule: false,
                        name:'[hash:10].[ext]',
                        outputPath:'imgs'  //打包后图片会输出到打包目录下的 imgs 文件夹中
                    }
                },
                //处理 html 中 img 资源
                {
                    test:/\.html$/,
                    loader:'html-loader'
                },
                //处理其他资源
                {
                    exclude:/\.(css|js|html|less|jpg|png|gif)/,
                    loader:'file-loader',
                    options:{
                        name:'[hash:10].[ext]',
                        outputPath:'media'  //打包后图片会输出到打包目录下的 media 文件夹中
                    }
                }
            ]
        }
    ]
},
缓存 ———— babel 缓存

问题:修改某个 js 文件后其他未变动的 js 无需重新编译,但 HMR 是基于 devServer 的,所以在生产环境中无法使用

解决:babel 缓存————让第二次打包构建速度更快

只需在 babel-loader 中进行设置

{
    test:/\.js$/,
    exclude: /node_modules/,
    loader:'babel-loader',
    options:{
        presets:[
            [
                '@babel/preset-env',
                {
                    useBuiltIns: 'usage',
                    corejs: {
                        version: 3
                    },
                    targets: {
                        chrome: '60',
                        firefox: '60',
                        ie: '9',
                        safari: '10',
                        edge: '17'
                    }
                }
            ]
        ],
        //开启 babel 缓存,第二次构建时会读取之前的缓存
        cacheDirectory: true
    }
},
多进程打包

通过 thread-loader 库进行多进程打包,在webpack.config.js 配置文件的 babel-loader 中添加 thread-loader

进程启动大概为 600ms,进程通信也有开销,只有工作消耗事件比较长,才需要多进程打包,若乱用反而会增加打包时间

一般 js 代码较多 babel 干的活久时使用多进程打包的加速效果更明显

{
    test:/\.js$/,
    exclude: /node_modules/,
    use: [
        //开启多进程打包
        {
            loader:'thread-loader',
            options:{
                workers: 2 //2个进程
            }
        },
        {
            loader:'babel-loader',
            options:{
                presets:[
                    [
                        '@babel/preset-env',
                        {
                            useBuiltIns: 'usage',
                            corejs: {
                                version: 3
                            },
                            targets: {
                                chrome: '60',
                                firefox: '60',
                                ie: '9',
                                safari: '10',
                                edge: '17'
                            }
                        }
                    ]
                ],
                //开启 babel 缓存,第二次构建时会读取之前的缓存
                cacheDirectory: true
            }
        }
    ]
},
externals

externals 防止将某些包打包到最终输出的 bundle 中,如想要 jQuery 通过 CDN 链接引入,就要禁止将它打包

在 webpack.config.js 文件中添加 externals

externals: {
    //忽略库名(npm 包名)
    jquery: 'jQuery' //拒绝 jQuery 被打包进来
} 

不打包就要在 html 中通过 <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> 引入 jQuery 才能使用

DLL

DLL 可以对代码进行单独打包

和 externals 类似,DLL 也是禁止一些库被打包进来,不同的是 DLL
会对一些库进行单独打包,在上面的 code split 代码分割中是把 node_modules 所有第三方库打包成一个 chunk,而 DLL 会把不同库分开打包

externals 中还需要通过 CDN 引入第三方库,而 DLL 会自动引入自己打包的第三方库

使用 dll 技术对某些第三方库(如 jQuery、react、vue…)进行单独打包

需要新建文件 webpack.dll.js 写入如下内容

const {resolve} = require('path');
const webpack = require('webpack');
module.exports = {
    entry: {
        //打包生成的名字[name]: ['要打包的库']
        jquery: ['jquery']
    },
    output: {
        filename: '[name].js',
        path: resolve(__dirname,'dll'),
        library: '[name]_[hash]', //打包的库里向外暴露出去的内容叫什么名字
    },
    plugins: [
        //打包生成一个 manifest.json 告诉 webpack 说 jQuery 不需要打包,并且提供和 jQuery 映射
        new webpack.DllPlugin({
            name: '[name]_[hash]',  //映射库的暴露的内容名称
            path: resolve(__dirname,'dll/manifest.json')  //最终这个库输出到哪里去
        })
    ],
    mode: 'production'
}

运行打包 webpack --config webpack.dll.js,因为运行 webpack 时默认查找 webpack.config.js 配置文件,要想运行 webpack.dll.js 配置,需要使用 –config 参数指定

打包后会生成 dll/jquery.js 和 dll/manifest.json,只需打包一次,此后 jQuery 无需再重复打包

下载 add-asset-html-webpack-plugin 包

npm i add-asset-html-webpack-plugin -D

在 webpack.config.js 文件中同样引入 webpack 和 add-asset-html-webpack-plugin

const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

在 webpack.config.js 文件 的 plugins 中添加

//告诉 webpack 哪些库不参与打包,同时使用时的名称也得变
new webpack.DllReferencePlugin({
    manifest: resolve(__dirname,'dll/manifest.json')
})
//将某个文件打包输出出去,并在 html 中自动引入该资源
new AddAssetHtmlWebpackPlugin({
    filepath: resolve(__dirname,'dll/jquery.js')
})

通过上述配置就会把 jQuery 库单独打包,并在 html 中自动引入自己打包的 jQuery 库,无需再手动通过 script 标签通过 CDN 引入 jQuery

(2)优化代码运行的性能

缓存 ———— 文件资源缓存

文件资源缓存————让代码上线运行缓存更好使用

可在打包输出的 built.js 文件名以及 css 文件名中添加 webpack 每次构建时生成的唯一 hash 值,当文件改变重新打包后生成的文件名改变则必须重新请求,不会读取缓存中的文件

有三种 hash 值:hash、chunkhash、contenthash

hash:每次 webpack 构建时会生成一个 hash 值
    问题:因为 js 和 css 同时使用一个 hash 值,若重新打包会导致所有缓存失效(可能只改动了一个文件)
chunkhash:根据 chunk 生成的 hash 值,若打包来源于同一个 chunk,则 hash 值就一样
    问题:因为 css 是在 js 中被引入的,所以同属于一个 chunk,打包后都在 built.js 中,所以 js 和 css 文件的 chunkhash 还是一样的
contenthash:根据文件内容生成 hash 值,不同文件的 hash 值一定不一样

更改后配置文件如下

const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
process.env.NODE_ENV = 'production';
//复用 loader
const commonCssLoader = [
    MiniCssExtractPlugin.loader,
    'css-loader',
    {
        //还需要在 package.json 中定义 browserslist
        loader:'postcss-loader',
        options:{
            ident: 'postcss',
            plugins: () => [
                require('postcss-preset-env')()
            ]
        }
    }
]
module.exports = {
    entry:'./src/index.js',
    output:{
        filename: 'js/built.[contenthash:10].js',  //添加 contenthash
        path: resove(__dirname,'build')
    },
    module:{
        rules:[
            //js 语法检查
            {
                //还需在 package.json 中配置 eslintConfig
                test:/\.js$/,
                exclude: /node_modules/,
                enforce: 'pre', //优先执行
                loader:'eslint-loader',
                options:{
                    fix:true
                }
            },
            {
                //以下 loader 只会匹配一个,不能有两个配置处理同一种类型文件
                oneOf:[
                    //处理 css
                    {
                        test:/\.less$/,
                        use:[...commonCssLoader]
                    },
                    //处理 less
                    {
                        test:/\.css$/,
                        use:[...commonCssLoader,'less-loader']
                    },
                    // js 兼容性处理
                    {
                        test:/\.js$/,
                        exclude: /node_modules/,
                        loader:'babel-loader',
                        options:{
                            presets:[
                                [
                                    '@babel/preset-env',
                                    {
                                        useBuiltIns: 'usage',
                                        corejs: {
                                            version: 3
                                        },
                                        targets: {
                                            chrome: '60',
                                            firefox: '60',
                                            ie: '9',
                                            safari: '10',
                                            edge: '17'
                                        }
                                    }
                                ]
                            ],
                            //开启 babel 缓存,第二次构建时会读取之前的缓存
                            cacheDirectory: true
                        }
                    },
                    //处理图片资源
                    {
                        test:/\.(jpg|png|gif)$/,
                        loader:'url-loader',
                        options:{
                            limit:8*1024,
                            esModule: false,
                            name:'[hash:10].[ext]',
                            outputPath:'imgs'  //打包后图片会输出到打包目录下的 imgs 文件夹中
                        }
                    },
                    //处理 html 中 img 资源
                    {
                        test:/\.html$/,
                        loader:'html-loader'
                    },
                    //处理其他资源
                    {
                        exclude:/\.(css|js|html|less|jpg|png|gif)/,
                        loader:'file-loader',
                        options:{
                            name:'[hash:10].[ext]',
                            outputPath:'media'  //打包后图片会输出到打包目录下的 media 文件夹中
                        }
                    }
                ]
            }
        ]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            minify: {
                collapseWhitespace: true,
                removeComments: true
            }
        });
        new MiniCssExtractPlugin({
            filename: 'css/built.[contenthash:10].css'  //添加contenthash
        }),
        //压缩 css
        new OptimizeCssAssetsWebpackPlugin()
    ],
    //模式
    mode: 'production',
    devtool: 'source-map'
}

服务器端代码

const express = require('express');
//创建应用对象
const app = express();
app.use(express.static('build',{maxAge:1000 * 3600}));
//监听端口启动服务
app.listen(8000)
tree shaking

tree shaking 用于去除无用代码

tree shaking 前提:(1)必须使用 ES6 模块化(2)mode 使用 production 环境即可自动开启 tree shaking

tree shaking 作用:减少代码体积,从而使得请求和加载速度更快

注意:在旧版本 webpack 中或设置 "sideEffects": false 表示所有代码都没有副作用,即都可以进行 tree shaking,这可能把 css 或 @babel/polyfill 等副作用文件都消除掉,因此需在 package.json 中配置 "sideEffects": ["*.css","*.less"]

code split

通过 code split 代码分割把打包后的文件分成多个,这样在加载时可并行加载速度更快,且可按需加载

代码分割主要是关注 js 文件

方式一:多入口

通过在 entry 中设置多入口来拆分文件(之前的单入口方式一般用在单页面应用,多入口用于多页面应用)

entry:{
    //多入口:每个入口最终都输出一个 bundle
    index: './src/js/index.js',
    test: './src/js/test.js'
},
output:{
    filename: 'js/[name].[contenthash:10].js'
    path: resolve(__dirname,'build')
}

方式二:splitChunks

若使用单入口 + splitChunks 可以将 node_modules 中代码(第三方代码)单独打包一个 chunk 输出

若使用多入口 + splitChunks 还会自动分析多入口 chunk 中有没有公共的文件,若有则只会把该公共文件打包成一个单独的 chunk 不会重复打包多次

entry:{
    index: './src/js/index.js',
    test: './src/js/test.js'
},
output:{
    filename: 'js/[name].[contenthash:10].js'
    path: resolve(__dirname,'build')
},
optimization: {
    splitChunks: {
        chunks: 'all'
    }
},

上述配置中若 index.js 和 test.js 都引入了 jQuery,则打包后会生成三个 bundle 分别对应 index.js、test.js 和 jQuery

方式三:import 动态导入语法

修改 js 代码,使用 impor 动态导入语法,让某个文件被单独打包成一个 chunk

在某 js 文件(如 index.js)中引入另一 js 文件时不使用 import {mul,red} from './xxx.js',而使用如下方式

import(/* webpackChunkName: '打包后的文件名' */'./xxx.js)
    .then(({mul,red}) => {
        //eslint-disable-next-line
        console.log(mul(1,2));
    })
    .cache(() => {
        //eslint-disable-next-line
        console.log('文件加载失败');
    });

其中 xxx.js 中定义了 mul 和 red 两个方法

export function mul(x, y) {
    return x * y;
}
export function red(x, y) {
    return x - y;
}

打包后会生成两个 bundle 分别对应 index.js 和 xxx.js

注意:一般多入口使用较少,会使用单入口 + splitChunks,并把其他要单独打包的 js 文件使用 import 动态导入语法来完成

js 的懒加载和预加载

懒加载:当文件需要使用时才加载,是通过把代码分割的 import 动态导入语法放在一个异步函数中来实现

预加载:在使用之前提前加载 js 文件,但兼容性较差

正常加载是并行加载,即同一时间加载多个文件,而预加载是等其他资源加载完毕,浏览器空闲了再偷偷加载资源

如在 index.js 中编写点击按钮触发 xxx.js 中定义的方法

document.getElementById('btn').onclick = function(){
    //懒加载和预加载(webpackPrefetch)
    import(/* webpackChunkName: 'xxx', webpackPrefetch: true */'./xxx.js)
    .then(({mul}) => {
        //eslint-disable-next-line
        console.log(mul(1,2));
    })
}
PWA

PWA(渐进式网络应用开发程序)让网页向 app 一样可以离线访问,性能也会更好

PWA 通过 workbox 实现,在 webpack 中通过使用 workbox-webpack-plugin 插件

下载后,在 webpack.config.js 文件中引入 workbox-webpack-plugin

const WorkboxWebpackPlugin = require('workbox-webpack-plugin')

在 webpack.config.js 文件中的 plugins 里添加

new WorkboxWebpackPlugin.GenerateSW({
    clientsClaim: true, //删除旧的 serviceworker
    skipWaiting: true //帮助 serviceworker 快速启动
})//会生成一个 serviceworker 配置文件

在 index.js 中添加如下代码

if('serviceWorker' in navigator){
    window.addEventListener('load',() => {
        navigator.serviceWorker
            .register('/service-worker.js') //注册 serviceworker
            .then(() => {
                console.log('sw注册成功');
            })
            .catch(() => {
                console.log('sw注册失败');
            });
    });
}

eslint 不认识 window、navigator 全局变量,因此需要在 package.json 中 eslintConfig 修改为

“eslintConfig”:{
    "extends":"airbnb-base",
    "env": {
        "browser": true
    }
}

serviceworker 代码必须运行在服务器上,这里通过 serve 包快速构建一个服务器进行测试,下载 serve 包 npm -i serve -g,再启动服务器 serve -s build 其中 build 表示打包后的目录,将 build 目录下所有资源作为静态资源暴露出去

二、webpack 配置详解

1、entry

entry 的值可以是 string、array、object,一般用 string 和 object 较多

string:单入口 ———— ‘./src/index.js’

打包形成一个 chunk,输出一个 bundle 文件,此时 chunk 名称默认是 main

array:多入口 ———— [‘./src/index.js’,’./src/xxx.js’]

所有入口文件最终只会形成一个 chunk,输出只有一个 bundle 文件 默认是 main.js(当 output 中 filename 是 [name].js 时)

一般用在 HMR 功能中让 html 热更新生效

object:多入口 ———— {index: ‘./src/index.js’, xxx: ‘./src/xxx.js’}

有几个入口文件就形成几个 chunk,输出几个 bundle 文件,此时 chunk 名称是 key

特殊用法:

entry: {
    index:['./src/index.js','./src/mul.js'], //mul 和 index 形成一个 chunk 打包输出在一个 bundle 文件中
    add: './src/add.js'  //形成一个 chunk,输出一个 bundle
}

2、output

output 中可以设置

filename ———— 文件名称(指定该名称 + 目录)

filename: 'js/[name].js'

path ———— 输出文件目录(将来所有资源输出的公共目录)

path: resolve(__dirname,'build')

publicPath ———— 所有资源引入时的公共路径前缀,一般用于生产环境

publicPath: '/' //当引入 imgs/a.jpg 时路径为 /imgs/a.jpg

chunkFilename ———— 非入口 chunk 的名称(entry 中指定的就是入口文件)

如使用 import 动态导入语法进行代码单独打包时,打包输出的文件按 chunkFilename 指定的规则命名,若不修改名称默认使用 filename,这会与入口文件的输出文件名称冲突,webpack 会自动修改导入文件的打包后文件名称,命名方式可能不方便使用

chunkFilename: 'js/[name]_chunk.js'

library ———— 整个库向外暴露的变量名,一般配合 dll 使用

library: '[name]'

libraryTarget ———— 变量名添加到哪里

libraryTarget: 'window' //将库添加到浏览器的 window 上
libraryTarget: 'global' //添加到 node 上
libraryTarget: 'commonjs'
libraryTarget: 'amd'

3、module

module: {
    rules: [
        //loader 配置
        {
            test: /\.css$/
            //多个 loader 用 use
            use: ['style-loader',''css-loader]
        },
        {
            test: /\.js$/
            //排除 node_modules 下的 js 文件
            exclude: /node_modules/,
            //只检查 src 目录下的 js 文件
            include: resolve(__dirname, 'src'),
            //优先执行
            enforce: 'pre',
            //延后执行 enforce: 'post',
            //单个 loader 用 loader
            loader: 'eslint-loader',
            options: {}
        },
        {
            //以下配置只会生效一个
            oneOf: []
        }
    ]
}

4、resolve

resolve 用于解析模块的规则

参数 alias ———— 配置解析模块路径别名:优点简写路径,缺点路径没有提示

参数 extensions ———— 配置省略文件路径的后缀名,默认值是 .js 和 .json

参数 modules ———— 告诉 webpack 解析模块是去哪个目录找加快解析速度

在 webpack.config.js 中添加

resolve:{
    alias: {
        $css: resolve(__dirname, 'src/css')
    },
    extensions: ['.js','.json', '.jsx', '.css']
    modules: [resolve(__dirname, '../../node_modules'), 'node_modules']
}

5、devServer

devServer 用于开发环境

devServer:{
    contentBase:resolve(__dirname,'build'), //要运行的项目构建后的目录
    watchContentBase: true, //监视 contentBase 目录下的所有文件,一旦文件变化就会 reload
    watchOptions: {
        //忽略文件
        ignored: /node_modules/
    }
    compress:true,  //启动 gzip 压缩,使代码更小运行更快
    port:5000,  //端口号
    open:true,  //自动打开浏览器
    hot: true,  //开启 HMR 功能
    clientLogLevel: 'none', //不显示启动服务器日志信息
    quiet: true, //除了一些基本启动信息外,其他内容都不要显示
    overlay: false, //如果出错不要全屏提示,在日志中打印出来就好
    proxy: { //服务器代理,解决开发环境跨域问题(浏览器和服务器间有跨域问题,服务器和服务器间没有跨域问题)
        '/api': {
            target: 'http://localhost:3000', //一旦 devServer(5000)服务器接收到 /api/xxx 的请求,就会把请求转发到另一个服务器(3000)
            pathRewrite: { //发送请求时,请求路径重写
                '^/api':'' //将 /api/xxx 重写为 /xxx(去掉 /api)
            }
        }
    }
}

6、optimization

optimization 在生产环境中才有意义

参数 splitChunks 用于提取公共代码成单独 chunk 打包

参数 runtimeChunk 将当前模块中记录其他模块 hash 的部分单独打包为一个文件,当 a 模块发生变化时 a 文件以及对应的 runtime 文件重新打包,而其他引用 a 的模块不重新打包 ———— 解决修改 a 文件导致 b 文件的 contenthash 变化产生的缓存失效需要重新打包 b 的问题

因此写代码分割时一定要加上 runtimeChunk,否则会导致缓存失效

参数 minimizer 配置生产环境的压缩方案(针对 js 和 css)

在 webpack 4.26 之前是使用 UglifyJsPlugin 插件进行 js 压缩,之后的版本是使用 terser-webpack-plugin 插件进行压缩

const TerserWebpackPlugin = require('terser-webpack-plugin');
optimization: {
    splitChunks: {
        chunks: 'all',
        //下面设置的这些都是用的默认值
        minSize: 30 * 1024, //分割的 chunk 最小为 30kb(即文件大于 30kb 才会分割)
        minChunks: 1, //要提取的 chunk 最少被引用 1 次
        maxAsyncRequests: 5, //按需加载时并行加载在的文件最大数量(所以最多也只会打包成相应数量 chunk)
        maxInitialRequests: 3, //入口 js 文件最大并行请求数量
        automaticNameDelimiter: '~', //名称连接符
        name: true, //可以使用命名规则
        cacheGroups: { //分割 chunk 的组
            vendors: {
                test: /[\\/]node_modules[\\/]/, //node_modules 文件(也需满足上面规则如大小超过 30kb,至少被引用一次)会被打包到 vendors 组的 chunk 中,即会输出 vendors~xxx.js
                priority: -10  //打包的优先级
            },
            default:{
                minChunks: 2, //要提取的 chunk 最少被引用 2 次
                priority: -20, //打包优先级
                reuseExistingChunk: true //若当前要打包的模块和之前已经被提取的模块是同一个就会复用,而不是重新打包模块
            }
        }
    },
    runtimeChunk: {
        name: entrypoint => `runtime-${entrypoint.name}`
    },
    minimizer: [
        new TerserWebpackPlugin({
            cache: true, //开启缓存
            parallel: true, //开启多进程打包,速度更快
            sourceMap: true //启动 source-map
        })
    ]
},

三、webpack 5

1、使用 webpack 5

首先初始化 npm npm init

下载 webpack 5 npm i webpack@next webpack-cli -D

在 webpack 5 中 webpack.config.js 配置文件中只有 mode 是需要设置的其他可使用默认值,这不同于 webpack 4,在 webpack 4 中需要设置 entry、output、mode

同样使用命令 webpack 打包

webpack 5 中默认输出的目录是 dist

其中 webpack 5 中使用的默认值如下

entry: "./src/index.js"
output.path: path.resolve(__dirname, "dist")
output.filename: "[name].js"

2、webpack 5 重点关注的内容

(1)通过持久缓存提高构建性能

(2)使用更好的算法和默认值来改善长期缓存(如 hash 值的相关算法)

(3)通过更好的 tree shaking 和代码生产来改善 bundle 大小

(4)清除处于怪异状态的内部结构,同时在 v4 中实现功能而不引入任何重大更改

(5)通过引入重大更改来为将来的功能做准备,使我们能尽可能长时间使用 v5

3、一些特点

自动删除 Node.js Polyfills

早期 webpack 的目标是允许在浏览器中允许大多数 node.js 模块,但模块格局发生了变化,许多模块用途现在主要是为前端目的而编写的,webpack <= 4 附带了许多 node.js 核心模块的 polyfill,一旦模块使用任何核心模块(即 crypto 模块),这些模块就会自动应用,尽管这使使用 node.js 编写的模块变得容易,但它会将这些巨大的 polyfill 添加到包中,许多情况下,这些 polyfill 是不必要的,webpack 5 会自动停止填充这些核心模块,并专注于与前端兼容的模块

在 webpack 5 中尽可能尝试使用与前端兼容的模块。可为 node.js 核心模块手动添加一个 polyfill,错误消息会提示如何实现

chunk 和 模块 ID

添加了用于长期缓存的新算法(更优化的 hash 值算法),在生产模式下默认启用这些功能

chunkIds:"deterministic", moduleIds:"deterministic"

chunk ID

可以不使用 import(/* webpackChunkName:"name" */ "module") 在开发环境来为 chunk 命名,生产环境还是有必要的

webpack 内部有 chunk 命名规则,不再是以 id(0,1,2) 命名了

tree shaking 功能更加强大

(1) webpack 5 能处理嵌套模块的 tree shaking

//a.js
export const aa = 1;
export const bb = 2;

//b.js
import * as  a from './a';
export { a };

//c.js
import * as b from './b';
console.log(b.a.aa);

在生产环境中,a 模块暴露的 bb 会被删除(因为没有被用到)

(2) webpack 5 能处理多个模块间的关系

import { something } from './something';
function usingSomething(){
    return something;
}
export function test(){
    return usingSomething();
}

当设置了 “sideEffects”: false 时,一旦发现 test 方法没有使用,不但删除 test,还会删除 ./something

(3) webpack 5 能处理 Commonjs 模块化的 tree shaking(之前 webpack 4 中都是以 ES6 模块化和生产环境为例)

ouput

webpack 4 默认只能输出 ES5 代码,webpack 5 开始新增属性 output.ecmaVersion 可以生成 ES5 和 ES6/ES2015 代码

output.ecmaVersion: 2015

optimization 中 splitChunk

webpack 4 中

minSize: 30000;

webpack 5 中,可进行更精确的划分

minSize: {
    javascript: 30000,
    style: 50000
}

caching 缓存

webpack 4 中设置了 babel 缓存等

webpack 5 在 cache 中设置缓存

//配置缓存
cache: {
    //磁盘存储
    type:'filesystem',
    buildDependencies: {
        //当配置修改时,缓存失效
        config: [__filename]
    }
}

缓存将存储到 node_modules/.cache/webpack

监视输出文件

之前 webpack 总是在第一次构建时输出全部文件,重新构建时会只更新修改的文件

webpack 5 在第一次构建时会找到输出文件看是否有变化,从而决定要不要输出全部文件

其他

webpack 可以处理 js、json 资源,不能处理 css、img 等其他资源