Webpack笔记

104 阅读9分钟

Webpack

npm i -D  // npm install --save-dev  开发环境
npm i -S // npm install --save  生产环境
  • webpack.base.js 定义公共的配置
  • webpack.dev.js:定义开发环境的配置
  • webpack.prod.js:定义生产环境的配置

webpack配置

build

http_env 为参数,npm run build2运行后会被赋值在node全局配置process.env

// package.json
scripts: {
    "serve": "vue-cli-service serve",
    "build": "webapck --config ./webpack.config.js",
    "lint": "vue-cli-service lint",
    "build2": "cross-env  http_env=qa node ./build/webpack.prod2.config.js"
}
// webpack.prod2.config.js
const ENV = process.env.http_env
console.log("2:", ENV);

Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。

Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。

mode:提供 mode 配置选项,告知 webpack 使用相应模式的内置优化

Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。

Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

Loader:模块转换器,用于把模块原内容按照需求转换成新内容。

Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。

webpack-dev-server 热更新

module.exports = {
  // ...省略其他配置
  devServer:{
    port:3000,
    hot:true,
    contentBase:'../dist'
  },
  plugins:[
    new Webpack.HotModuleReplacementPlugin()
  ]
}

npm i -S vue

loader

用于对模块源码的转换,loader 描述了 webpack 如何处理非 javascript 模块,并且在 buld 中引入这些依赖。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或者将内联图像转换为 data URL。比如说:CSS-Loader,Style-Loader 等。

babel-loader

将JS代码向低版本转换

bable-loader @bable/preset-env @bable/core // 将es6,7,8 转换为es5

@bable/polyfill 转换 promise, proxy, Set, Maps等新api

style-loader css-loader

style-loader:动态创建 style 标签,将webpack处理之后的css内容插入到HTML的HEAD标签里

css-loader:解析css文件中的@import依赖关系

less less-loader

负责处理编译 .less 文件,将其转为 css

 module: {
        rules: [
            {
                test: /.(le|c)ss$/,
                use: ['style-loader', 'css-loader', {
                    loader: 'postcss-loader',
                    options: {
                        plugins: function () {
                            return [
                                require('autoprefixer')({
                                    "overrideBrowserslist": [
                                        ">0.25%",
                                        "not dead"
                                    ]
                                })
                            ]
                        }
                    }
                }, 'less-loader'],
                exclude: /node_modules/
            }
        ]
    } 
​
loader优先级

loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行,上面 loader 的执行顺序为: less-loader ---> postcss-loader ---> css-loader ---> style-loader

文件压缩file-loader url-loader

图片/字体文件处理

url-loaderfile-loader 的功能类似,但是 url-loader 可以指定在文件大小小于指定的限制时,返回 DataURL,因此,个人会优先选择使用 url-loader

//webpack.config.js
module.exports = {
    //...
    modules: {
        rules: [
            {
                test: /.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 10240, //10K
                            esModule: false 
                        }
                    }
                ],
                exclude: /node_modules/
            }
        ]
    }
}

html-withimg-loader

解决index.html文件中引用相对路径图片文件,然后打包后找不到相对路径中的文件问题。(因为打完包后路径可能会被改变)

如果file-loader 的版本是5版本之后( 如5.0.2 ),需要在file-loader中增加 esModule 属性

module.exports = {
    //...
    module: {
        rules: [
            {
                test: /.html$/,
                use: 'html-withimg-loader'
            }
        ]
    }
}

vue-loader vue-template-compiler vue-style-loader

解析vue

Plugin

目的在于解决 loader 无法实现的其他事,它直接作用于 webpack,扩展了它的功能。在 webpack 运行的生命周期中会广播出许多事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

autoprefixer postcss-loader

autofixer是postcss的功能插件,主要是给css中的一些属性添加-webkit-这种前缀做兼容的

postcss-loader则是webpack的loader组件,主要作用是webpack在读取css模块的时候调用postcss和postcss的插件处理css内容的。所以会有postcss-loader配置options的过程实际上是为postcss配置需要的插件

extract-text-webpack-plugin

webpack 默认会将 css 当做一个模块打包到一个 chunk 中,extract-text-webpack-plugin 的作用就是将 css 提取成独立的 css 文件

const ExtractTextPlugin = require('extract-text-webpack-plugin');
new ExtractTextPlugin({
    filename: 'css/[name].css',
})
{
    test: /.css$/,
    use: ExtractTextPlugin.extract({
        use: ['css-loader','postcss-loader','less-loader'],
        fallback: 'vue-style-loader',  #使用vue时要用这个配置
    })
}

mini-css-extract-plugin

mini-css-extract-pluginextract-text-webpack-plugin 相比:

  1. 异步加载
  2. 不会重复编译(性能更好)
  3. 更容易使用
  4. 只适用CSS
//webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/[name].css'
            //个人习惯将css文件放在单独目录下
            //publicPath:'../'   //如果你的output的publicPath配置的是 './' 这种相对路径,那么如果将css文件放在单独目录下,记得在这里指定一下publicPath 
        })
    ],
    module: {
        rules: [
            {
                test: /.(le|c)ss$/,
                use: [
                    MiniCssExtractPlugin.loader, //替换之前的 style-loader
                    'css-loader', {
                        loader: 'postcss-loader',
                        options: {
                            plugins: function () {
                                return [
                                    require('autoprefixer')({
                                        "overrideBrowserslist": [
                                            "defaults"
                                        ]
                                    })
                                ]
                            },
                            hmr: isDev,
                            reloadAll: true
                        }
                    }, 'less-loader'
                ],
                exclude: /node_modules/
            }
        ]
    }
}

optimize-css-assets-webpack-plugin

将抽离出来的css文件进行压缩

使用 mini-css-extract-pluginCSS 文件默认不会被压缩,如果想要压缩,需要配置 optimization,首先安装 optimize-css-assets-webpack-plugin.

修改webpack配置:

//webpack.config.js
const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin');
​
module.exports = {
    entry: './src/index.js',
    //....
    plugins: [
        new OptimizeCssPlugin()
    ],
}

html-webpack-plugin

html-webpack-plugin 这个插件很重要

作用一是创建 HTML 页面文件到你的输出目录

作用二是将 webpack 打包后的 chunk 自动引入到这个 HTML 中

const HtmlPlugin = require('html-webpack-plugin')
new HtmlPlugin({
    filename: 'index.html',
    template: 'pages/index.html'
}

copy-webpack-plugin

静态资源拷贝

有些时候,我们需要使用已有的JS文件、CSS文件(本地文件),但是不需要 webpack 编译。例如,我们在 public/index.html 中引入了 public 目录下的 jscss 文件。这个时候,如果直接打包,那么在构建出来之后,肯定是找不到对应的 js / css 了。

├── public
│   ├── config.js
│   ├── index.html
│   ├── js
│   │   ├── base.js
│   │   └── other.js
│   └── login.html

现在,我们在 index.html 中引入了 ./js/base.js

<!-- index.html -->
<script src="./js/base.js"></script>

这时候,我们 npm run dev,会发现有找不到该资源文件的报错信息。

使用copy-webpack-pluin, 修改配置(当前,需要做的是将 public/js 目录拷贝至 dist/js 目录):

//webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
    //...
    plugins: [
        new CopyWebpackPlugin([
            {
                from: 'public/js/*.js',
                to: path.resolve(__dirname, 'dist', 'js'),
                flatten: true,
            },
            //还可以继续配置其它要拷贝的文件
        ])
    ]
}

按需加载

import() 语法,需要 @babel/plugin-syntax-dynamic-import 的插件支持,但是因为当前 @babel/preset-env 预设中已经包含了 @babel/plugin-syntax-dynamic-import,因此我们不需要再单独安装和配置。

webpack 遇到 import(****) 这样的语法的时候,会这样处理:

  • 以**** 为入口新生成一个 Chunk
  • 当代码执行到 import 所在的语句时,才会加载该 Chunk 所对应的文件(如这里的1.bundle.8bf4dc.js)

大家可以在浏览器中的控制台中,在 NetworkTab页 查看文件加载的情况,只有点击之后,才会加载对应的 JS

热更新

  1. 首先配置 devServerhottrue
  2. 并且在 plugins 中增加 new webpack.HotModuleReplacementPlugin()
//webpack.config.js
const webpack = require('webpack');
module.exports = {
    //....
    devServer: {
        hot: true,
        
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin() //热更新插件
    ]
}

我们配置了 HotModuleReplacementPlugin 之后,会发现,此时我们修改代码,仍然是整个页面都会刷新。不希望整个页面都刷新,还需要修改入口文件:

  1. 在入口文件中新增:
if(module && module.hot) {
    module.hot.accept()
}

多页应用打包

//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry: {
        index: './src/index.js',
        login: './src/login.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[hash:6].js'
    },
    //...
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html' //打包后的文件名
        }),
        new HtmlWebpackPlugin({
            template: './public/login.html',
            filename: 'login.html' //打包后的文件名
        }),
    ]
}
​

如果需要配置多个 HtmlWebpackPlugin,那么 filename 字段不可缺省,否则默认生成的都是 index.html,如果你希望 html 的文件名中也带有 hash,那么直接修改 fliename 字段即可,例如: filename: 'login.[hash:6].html'

次打包方法index.html 和 login.html会都同时引入了 index.f7d21a.jslogin.f7d21a.js,我们希望,index.html 中只引入 index.f7d21a.jslogin.html 只引入 login.f7d21a.js

HtmlWebpackPlugin 提供了一个 chunks 的参数,可以接受一个数组,配置此参数仅会将数组中指定的js引入到html文件中,此外,如果你需要引入多个JS文件,仅有少数不想引入,还可以指定 excludeChunks 参数,它接受一个数组。

plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html', //打包后的文件名
            chunks: ['index']
        }),
        new HtmlWebpackPlugin({
            template: './public/login.html',
            filename: 'login.html', //打包后的文件名
            chunks: ['login']
        }),
    ]

定义环境变量

很多时候,我们在开发环境中会使用预发环境或者是本地的域名,生产环境中使用线上域名,我们可以在 webpack 定义环境变量,然后在代码中使用。

使用 webpack 内置插件 DefinePlugin 来定义环境变量。

DefinePlugin 中的每个键,是一个标识符.

  • 如果 value 是一个字符串,会被当做 code 片段
  • 如果 value 不是一个字符串,会被stringify
  • 如果 value 是一个对象,正常对象定义即可
  • 如果 key 中有 typeof,它只针对 typeof 调用定义
const webpack = require("webpack");
module.exports = {
    plugins: [
        new webpack.DefinePlugin({
            DEV: JSON.stringify("dev"),
            FLAG: "false" // FLAG是布尔类型
        })
    ]
}
​
// index.js
if(DEV === 'dev') {
​
} else if(FLAG === false) {
​
}

webpack解决跨域

// server.js
let express = require('express');
​
let app = express();
​
app.get('/api/user', (req, res) => {
    res.json({name: '刘小夕'});
});
​
app.listen(4000);
配置代理

index.js 中请求 /api/user,修改 index.js 如下:

//需要将 localhost:3000 转发到 localhost:4000(服务端) 端口
fetch("/api/user")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));

我们希望通过配置代理的方式,去访问 4000 的接口。

修改 webpack 配置:

// webpack.config.js
module.explorts = {
    devServer: {
        proxy: {
            '/api': 'http://localhost:4000'
        }
    }
}

mocker-api数据接口

module.exports = {
    'GET /user': {name: '刘小夕'},
    'POST /login/account': (req, res) => {
        const { password, username } = req.body
        if (password === '888888' && username === 'admin') {
            return res.send({
                status: 'ok',
                code: 0,
                token: 'sdfsdfsdfdsf',
                data: { id: 1, name: '刘小夕' }
            })
        } else {
            return res.send({ status: 'error', code: 403 })
        }
    }
}

CommonsChunkPlugin

主要是用来提取第三方库(如 jQuery)和公共模块(公共 js,css 都可以),常用于多页面应用程序,生成公共 chunk,避免重复引用。

{
    entry: {
        vendor: 'index.js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor','runtime'],
            filename: '[name].js'
        }),
    ]
}

\

优化

speed-measure-webpack-plugin

speed-measure-webpack-plugin 插件可以测量各个插件和loader所花费的时间,使用之后,构建时,会得到类似下面这样的信息:

//webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
​
const config = {
    //...webpack配置
}
​
module.exports = smp.wrap(config);

cache-loader

在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。

happypack

HappyPack 就能让 Webpack 做到同一时刻处理多个任务解决构建慢的问题,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。

npm install happypack -D

const Happypack = require("happypack");
module.explorts = {
    module: {
        rules: [
            {
                test: /.js[x]?$/,
                use: "Happypack/loader?id=js",
                include: [path.resolve(__dirname, 'src')]
            },
            {
                test: /.css$/,
                use: "Happypack/loader?id=css",
                include: [
                    path.resolve(__dirname, 'src'),
                    path.resolve(__dirname, 'node_modules', 'bootstrap')
                ]
            }
        ]
    },
    plugins: [
        new Happypack({
            id: 'js', // 和rules的id=js对应
            use: ['babel-loader'] //必须是数组
        }),
        new Happypack({
            id: 'css',//和rule中的id=css对应
            use: ['style-loader', 'css-loader','postcss-loader'],
        })
    ]
}

thread-loader

除了使用 Happypack 外,我们也可以使用 thread-loaderthread-loaderHappypack 我对比了一下,构建时间基本没什么差别。不过 thread-loader 配置起来。

module.exports = {
    module: {
        //我的项目中,babel-loader耗时比较长,所以我给它配置 thread-loader
        rules: [
            {
                test: /.jsx?$/,
                use: ['thread-loader', 'cache-loader', 'babel-loader']
            }
        ]
    }
}

开启 JS 多进程压缩

不管是 webpack-parallel-uglify-plugin 或者是 uglifyjs-webpack-plugin 配置 parallel。不过这里我要说一句,没必要单独安装这些插件,它们并不会让你的 Webpack 构建速度提升。

当前 Webpack 默认使用的是 TerserWebpackPlugin,默认就开启了多进程和缓存,构建时,你的项目中可以看到 terser 的缓存文件 node_modules/.cache/terser-webpack-plugin

noParse

如果一些第三方模块没有AMD/CommonJS规范版本,可以使用 noParse 来标识这个模块,这样 Webpack 会引入这些模块,但是不进行转化和解析,从而提升 Webpack 的构建性能 ,例如:jquerylodash

//webpack.config.js
module.exports = {
    //...
    module: {
        noParse: /jquery|lodash/
    }
}

配置noParse 前,构建需要 2392ms。配置了 noParse 之后,构建需要 1613ms。 如果你使用到了不需要解析的第三方依赖,那么配置 noParse 很显然是一定会起到优化作用的。

IgnorePlugin

webpack 的内置插件,作用是忽略第三方包指定目录。

moment (2.24.0版本) 会将所有本地化内容和核心功能一起打包,我们就可以使用 IgnorePlugin 在打包时忽略本地化内容。

//webpack.config.js
module.exports = {
    //...
    plugins: [
        //忽略 moment 下的 ./locale 目录
        new webpack.IgnorePlugin(/^./locale$/, /moment$/)
    ]
}

DllPlugin DllReferencePlugin

如果所有的JS文件都打成一个JS文件,会导致最终生成的JS文件很大,这个时候,我们就要考虑拆分 bundles

DllPluginDLLReferencePlugin 可以实现拆分 bundles,并且可以大大提升构建速度,DllPluginDLLReferencePlugin 都是 webpack 的内置模块。

DLLPlugin 这个插件是在一个额外独立的webpack设置中创建一个只有dll的bundle,也就是说我们在项目根目录下除了有webpack.config.js,还会新建一个webpack.dll.config.js文件。webpack.dll.config.js作用是把所有的第三方库依赖打包到一个bundle的dll文件里面,还会生成一个名为 manifest.json文件。 该manifest.json的作用是用来让 DllReferencePlugin 映射到相关的依赖上去的。

DllReferencePlugin 这个插件是在webpack.config.js中使用的,该插件的作用是把刚刚在webpack.dll.config.js中打包生成的dll文件引用到需要的预编译的依赖上来。什么意思呢?就是说在webpack.dll.config.js中打包后比如会生成 vendor.dll.js文件和vendor-manifest.json文件,vendor.dll.js文件包含所有的第三方库文件,vendor-manifest.json文件会包含所有库代码的一个索引,当在使用webpack.config.js文件打包DllReferencePlugin插件的时候,会使用该DllReferencePlugin插件读取vendor-manifest.json文件,看看是否有该第三方库。vendor-manifest.json文件就是有一个第三方库的一个映射而已。

所以说 第一次使用 webpack.dll.config.js 文件会对第三方库打包,打包完成后就不会再打包它了,然后每次运行 webpack.config.js文件的时候,都会打包项目中本身的文件代码,当需要使用第三方依赖的时候,会使用 DllReferencePlugin插件去读取第三方依赖库。所以说它的打包速度会得到一个很大的提升。

抽离公共代码

抽离公共代码是对于多页应用来说的,如果多个页面引入了一些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。

抽离公共代码对于单页应用和多页应该在配置上没有什么区别,都是配置在 optimization.splitChunks 中。

module.exports= {
    optimization: {
        splitChunks: { // 分割代码
            cacheGroups: {
                vendor: {
                    priority: 1,
                    name: "vendor",
                    test: "node_modules",
                    chunks: "initial",
                    mainSize: 0,
                    minChunks: 1
                },
                
                //缓存组
                common: {
                    chunks: 'initial',
                    name: 'common',
                    minSize: 100, //大小超过100个字节
                    minChunks: 3 //最少引入了3次
                },
                
                runtimeChunk: {
                    name: "manifest"
                }
            }
        }
    }
}
借助 webpack-bundle-analyzer 进一步优化

webpack-bundle-analyzer 查看哪些包的体积较大

npm install webpack-bundle-analyzer -D
//webpack.config.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = merge(baseWebpackConfig, {
    //....
    plugins: [
        //...
        new BundleAnalyzerPlugin(),
    ]
})

Vite 解决了 Webpack 哪些问题

随着项目的复杂度升级,代码规范和管理就必须要同步提升。于是,编程社区中提出了多种模块化规范,服务端选择了 CommonJS 规范,客户端选择 AMD 规范较多,但是,两种模块化规范也都存在一定的问题,都是 JS 编程,有两个不同的模块化规范,在 JS 语言层面还是不够的,终于在 ES6 中,ECMA 委员会推出了语言层面模块系统:ES Modules 规范

模块化可以帮助我们更好地解决复杂应用开发过程中的代码组织问题,但是随着模块化思想的引入,我们的前端应用又会产生了一些新的问题,比如:

  • 首先,我们所使用的 ES Modules 模块系统本身就存在环境兼容问题。尽管现如今主流浏览器的最新版本都支持这一特性,但是目前还无法保证用户的浏览器使用情况。所以我们还需要解决兼容问题
  • 其次,模块化的方式划分出来的模块文件过多,而前端应用又运行在浏览器中,每一个文件都需要单独从服务器请求回来。零散的模块文件必然会导致浏览器的频繁发送网络请求,影响应用的工作效率
  • 最后,谈一下在实现 JS 模块化的基础上的发散。随着应用日益复杂,在前端应用开发过程中不仅仅只有 JavaScript 代码需要模块化,HTML 和 CSS 这些资源文件也会面临需要被模块化的问题。而且从宏观角度来看,这些文件也都应该看作前端应用中的一个模块,只不过这些模块的种类和用途跟 JavaScript 不同

对于开发过程而言,模块化肯定是必要的,所以我们需要在前面所说的模块化实现的基础之上引入更好的方案或者工具,去解决上面提出的 3 个问题,让我们的应用在开发阶段继续享受模块化带来的优势,又不必担心模块化对生产环境所产生的影响

Webpack

  • 脚手架工具 vue-cli 使用 webpack 进行打包,开发时可以启动本地开发服务器,实时预览。因为需要对整个项目文件进行打包,开发服务器启动缓慢。
  • 而对于开发时文件修改后的热更新 HMR (热替换)也存在同样的问题
  • Webpack 的热更新会以当前修改的文件为入口重新 build 打包,所有涉及到的依赖也都会被重新加载一次

Vite 则很好地解决了上面的两个问题

vite 只启动一台静态页面的服务器,对文件代码不打包,服务器会根据客户端的请求加载不同的模块处理,实现真正的按需加载

Vite

Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题

热更新问题

vite 采用立即编译当前修改文件的办法。同时 vite 还会使用vite 内置缓存,加载更新后的文件内容

vite 构建项目与 vue-cli 构建的项目在开发模式下还是有比较大的区别:

  • Vite 在开发模式下不需要打包可以直接运行,使用的是 ES6 的模块化加载规则;Vue-CLI 开发模式下必须对项目打包才可以运行
  • Vite 基于缓存的热更新,Vue-CLI 基于 Webpack 的热更新