Webpack 学习

103 阅读4分钟

本文章记录在 b 站学习 webpack5 的学习笔记,视频传送门

课程收获

  1. Webpack 在 前端工程化领域 的作用及原理;参与项目的 打包配置

  2. 工程化层面优化 开发环境项目性能;落地 面向前端业务的技术方案

一、 概述

(概述内容参考文章:当面试官问Webpack的时候他想知道什么

webpack 是一种前端资源构建工具,当它处理应用程序时,会在内部从一个或多个入口点构建一个依赖图,然后将项目中所需的每一个模块组合成一个或多个 bundles,用于内容的展示。

其作用有以下几点:

  1. 模块打包:可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。

  2. 编译兼容:弱化了浏览器代码兼容的问题,通过 Loader 机制,帮助我们对代码做 polyfill,还可以编译转换诸如 .less、.vue、.jsx 这类在浏览器中无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提升开发效率。

  3. 能力扩展:通过 Plugin 机制,可以在实现模块化打包和编译兼容的基础上,进一步实现诸如按需加载、代码压缩等一系列功能,帮助我们进一步提高自动化程度、工程效率以及打包输出的质量。

二、核心概念

1. 入口(entry)

指示 webpack 应该使用哪个模块来作为构建其内部 依赖图(dependency graph) 的开始。进入入口起点后, webpack 会找出有哪些模块和库是入口起点依赖的。

2. 输出(output)

告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。

3. loader

webpack 只能理解 JavaScriptJSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换成有效的 模块,供应用程序使用,以及被添加到依赖图中。

4. 插件(plugin)

执行范围更广的任务,包括:打包优化资源管理注入环境变量

三、起步

1. 基本安装

mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev // 此命令用于在命令行中运行 webpack

2. 配置文件

在文件根目录下新建 webpack.config.js 配置文件。

// webpack.config.js
module.exports = {
  entry: './assets/js/main.js',
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: []
  },
  plugins: [],
  mode: 'development'
}

3. 打包命令

npx webpack
npm run build // npm scripts: "build": "webpack"

关于 npx 的命令

// 指定使用哪个 webpack 配置文件
npx webpack --config (-c) ./config/webpack.config.dev.js 
// 指定使用哪个 webpack 文件启动服务
npx webpack-dev-server --open
npx webpack serve -c ./config/webpack.config.dev.js
// 指定当前环境变量
npx webpack --env production 

四、管理资源

1. 加载样式

1.1 加载 CSS 样式

为了在 JavaScript 模块中引入一个 CSS 文件,需要安装 style-loadercss-loader,并且在 module 配置中添加这些 loader

npm install style-loader css-loader -D

webpack.config.js

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader'] // 书写有顺序要求,先保证 css 打包没有问题,再使用 style-loader 把 css 放置在页面上
            }
        ]
    }
}

模块 loader 可以链式调用。链中的每个 loader 都将对资源进行转换。链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。

  1. test: 识别出哪些文件会被转换。
  2. use: 定义出在进行转换时,应该使用哪个 loader
1.2 加载 less 资源
npm install less-loader -D

webpack.config.js

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    }
}
1.3 抽离 CSS 代码为独立文件

安装 mini-css-extract-plugin 插件

npm install mini-css-extract-plugin -D

webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
            }
        ]
    }
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'styles/[contenthash].css'
        })
    ]
}
  1. filename: 指定文件名和路径。
1.4 压缩 CSS 代码

安装 css-minimizer-webpack-plugin 插件

npm install css-minimizer-webpack-plugin -D

webpack.config.js

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
    ...
    optimization: {
        // 压缩 CSS 代码
        minimizer: [
            new CssMinimizerPlugin()
        ]
    },
    // 模式
    mode: 'production'
  1. 压缩 CSS 代码,只在生产模式下有效。

2. 加载 images 图像

webpack.config.js

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|jpeg|gif)$/,
                type: 'asset/resource', // 发送一个单独的文件并导出url,之前通过 file-loader 实现

                // 生成资源的路径和文件名,与 output 中的 assetModuleFilename 作用一致,两者同时存在时,generator 优先级高
                generator: {
                    filename: 'images/[contenthash][ext]'
                }
            },
        ]
    }
}

3. 加载 fonts 字体

webpack.config.js

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                type: 'asset/resource'
            }
        ]
    }
}

4. 加载数据

可以加载的有用资源还有数据,如 JSON 文件CSVTSVXML。类似于 NodeJS,JSON 支持实际上是内置的。要导入其他三种类型的数据,可以使用 csv-loaderxml-loader

npm install csv-loader xml-loader -D

webpack.config.js

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(csv|tsv)$/, // 加载不同类型的数据
                type: 'csv-loader'
            },
            {
                test: /\.xml$/, // 加载不同类型的数据
                type: 'xml-loader'
            }
        ]
    }
}

五、管理输出

1. 设置 HtmlWebpackPlugin

安装 html-webpack-plugin 插件

npm install --save-dev html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    ...
    plugins: [
        // 自动生成 html 入口文件和引用 js 文件的功能
        new HtmlWebpackPlugin({
            title: 'Output Management',
            template: './index.html',
            filename: 'app.html'
        })
    ],
}

2. 清理 /dist 文件夹

在每次构建前清理 /dist 文件夹,这样只会生成用到的文件,使用 output.clean 配置项实现

module.exports = {
    ...
    output: {
        clean: true
    }
}

六、开发环境

本章节旨在学习如何设置一个开发环境,使我们的开发体验变得更轻松。

1. 使用 source map

module.exports = {
   mode: 'development',
   devtool: 'inline-source-map' // 用来设置打包后代码的具体文件位置,精准定位代码的行数
};

2. 选择一个开发工具

每次编译代码时,手动运行 npm run build 比较麻烦,webpack 提供几种可选方式,帮助我们在代码发生变化时自动编译代码。(常用的是 webpack-dev-server)

2.1 webpack's Watch Mode
npm run webpack --watch // 使用 npm run webpack --watch 启动 webpack watch mode

缺点:需要刷新浏览器才能看到修改后的实际效果。

2.2 webpack-dev-server

webpack-dev-server 提供了一个基本的 web server,并且具有 live reloading(实时重新加载)功能

安装 webpack-dev-server 插件

npm install --save-dev webpack-dev-server
module.exports = {
    ...
    devServer: {
        static: './dist', // 告知 dev-server 从什么位置开始查找文件
        port: 3001, // 指定监听请求的端口号
        open: true, // 告知 dev-server 在服务器启动后打开浏览器
        proxy: { // 有单独的 API 后端开发服务器并希望在同一域上发送 API 请求
            '/api': 'http://localhost: 3001', // 将对 /api/users 的请求代理到 http: //localhost: 3001/api/users
            pathRewrite: { // 不希望传递 /api 时,重写路径
                '^/api': ''
            }
        },
        hot: true, // 启用热模块替换功能
        liveReload: true // 若生效,devServer.hot 必须禁用 或者 devServer.watchFiles 配置项必须启用
    }
}

七、代码分离

通过此特性,能够将代码分离到不同的 bundle 中,然后可以按需加载或者并行加载这些文件。可以用于获取更小的bundle,以及控制资源加载优先级。

代码分离的三种方式

优点:将多个文件共享的代码分离出去,减少入口文件的大小,从而提高首屏加载的速度。

  1. 配置入口起点:使用 entry 配置手动地分离代码(缺点:如果多个入口存在共享文件,会重复打包。)
  2. 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
  3. 动态导入:通过模块的内联函数调用来分离代码。

1. 防止重复

1.1 入口依赖

配置 dependOn option 选项,在多个 chunk 之间共享模块。

module.exports = {
    ...
    entry: {
        app: {
            import: './src/app.js',
            dependOn: 'shared'
        },
        web: {
            import: './src/another-module.js',
            dependOn: 'shared'
        },
        shared: 'lodash' // 将公共的代码抽离出来,生成单独的 bundle 文件
    },
}
1.2 SplitChunksPlugin
  • SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
module.exports = {
    ...
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    },
}
  • 分离 CSS 代码

安装 mini-css-extract-plugin 插件

npm install --save-dev mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 抽离 CSS
module.exports = {
    ...
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'styles/[contenthash].css' // 指定文件名和路径
        })
    ],
}

2. 动态导入

2.1 案例
function getComponent() {
    // 动态代码拆分时,使用 ECMAScript 提案的 import() 语法来实现
    return import('lodash')
    .then(({default: _}) => {
        const element = document.createElement('div');
        element.innerHTML = _.join(['Hello', 'webpack'], ' ')
        return element
    })
}

getComponent().then((element) => {
    document.body.appendChild(element);
    console.log('is async-module execute')
})
2.2 应用
  • 懒加载:按需加载

是一种很好的优化网页或者应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块完成操作后,立即引用或者即将引用另外一些新的代码块。这样有利于加快应用的初始加载速度,减轻总体体积,因为有些代码块可能永远不会被加载

  • 预获取/预加载模块

在声明 import 之前,使用内置的指令,可以让 webpack 输出 ‘resource hint’(资源提示),来告知浏览器。

prefetch(预获取)将来某些导航下可能需要的资源。

意义在于当首页内容都加载完毕之后,在网络空闲的时候,再去加载打包好的文件。这样既不会影响首屏加载速度,又省去了将来加载的延迟.

preload(预加载)当前导航下可能需要资源,实现页面模块的并行加载

// prefetch
button.addEventListener('click', () => {
    // 通过魔法注释修改打包后的文件名
    import( /* webpackChunkName: 'math', webpackPrefetch: true */ './math').then(({
        add
    }) => {
        console.log(add(4, 5));
    })
})

// preload
import(/* webpackPreload: true */ 'ChartingLibrary');

八、缓存

webpack 打包模块化后的应用程序,会生成一个可部署的 ./dist 目录,打包后的内容放置于此。只要把该目录的内容部署到 server 上,clint 就能够访问此 server 的网站及其资源。由于获取资源比较耗费时间,于是采用浏览器缓存,通过命中缓存,降低网络流量,使网站加载速度更快

1. 输出文件的文件名

如果部署新版本不更改资源的文件名,浏览器可能会认为它没有被更新,于是使用其缓存版本。为了杜绝这种问题发生,使用 substitution(可替换模板字符串) 的方式,保证文件名的唯一性。

module.exports = {
    ...
    output: [
        filename: '[name].[contenthash].js'
    ],
}

2. 缓存第三方的库

module.exports = {
    ...
    optimization: {
        runtimeChunk: 'single', // 将 runtime 代码拆分为一个单独的 chunk,设置为 single 来为所有 chuunk 创建一个 runtime bundle
        splitChunks: {
            // 将第三方的库单独提取到 vendor chunk 文件中,利用 client 的长效缓存机制,命中缓存来消除请求
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all'
                }
            }
        },
    },
}

九、资源模块

资源模块(Asset Module)是一种模块类型,允许在不额外配置 loader 的情况下使用图像、字体等资源文件,是 webpack 的内置模块。

十、生产环境

1. 配置

development(开发环境)和 production(生产环境)的构建目标存在着巨大差异。开发环境需要的是:强大的 source map 和一个有着 live reloadinghot module replacement 能力的 localhost server;而生产环境的关注点在于压缩 bundle、更轻量的 source map、资源优化等,通过这些优化方式改善加载时间。

遵循着不重复原则(Dont't Repeat Yourself - DRY),保留一个“common”配置,使用 webpack-merge 工具,将生产环境和开发环境的配置分别与通用配置合并在一起。

安装 webpack-merge 插件

npm install --save-dev webpack-merge
  • project
 webpack-demo
  |- package.json
  |- package-lock.json
  |- webpack.config.js
  |- webpack.common.js
  |- webpack.dev.js
  |- webpack.prod.js
  |- /dist
  |- /src
    |- index.js
    |- math.js
  |- /node_modules
  • webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry: {
        app: './src/index.js',
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Production',
        })
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
};
  • webpack.dev.js
module.exports = {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
        static: './dist'
    }
};
  • webpack.prod.js
module.exports = {
    mode: 'production'
};
  • webpack.config.js
const { merge } = require('webpack-merge'); // 合并配置文件
const commonConfig = require('./webpack.config.common');
const developmentConfig = require('./webpack.config.dev');
const productionConfig = require('./webpack.config.prod');

module.exports = (env) => {
    switch(env) {
        case env.production: 
            return merge(commonConfig, productionConfig);
        case env.development: 
            return merge(commonConfig, developmentConfig);
        default: 
            return new Error('No matching configuration was found!')
    }
}

模块解析(Resolve)

所引用的模块可以是来自应用程序的代码,也可以是第三方库。resolver 帮助 webpack 从每个 require / import 语句中,找到需要引入到 bundle 中的模块代码。当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。(webpack 基于 webpack_resolver 进行 treeshaking

外部扩展(Externals)

为了减小 bundle 体积,把一些不变的第三方库用 CDN 的形式引入进来。

十一、构建性能

1. 通用环境

  • 更新到最新版本;

Webpack、Node.js、npm 或 yarn 更新至最新版本,有助于提高性能。较新的版本能够建立更高效的模块树以及提高解析速度。

  • 将 loader 应用于最少数量的必要模块;
const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /.js$/,
        include: path.resolve(__dirname, 'src'),
        loader: 'babel-loader',
      },
    ],
  },
};
  • 引导

每个额外的 loader/plugin 都有其启动的时间,尽量少地使用工具;

  • 解析
  • dll
  • 小即是快(smaller = faster)

减少编译结果的整体大小,以提高构建性能。尽量保持 chunk 体积小。

  • 使用数量更少/体积更小的 library;
  • 在多页面应用程序中使用 SplitChunksPlugin,并开启 async 模式;
  • 移除未引用的代码;
  • 只编译当前正在开发的代码。
  • worker 池
  • 持久化缓存
  • 自定义 plugin/loader
  • Progress plugin

2. 开发环境

  • 增量编译
  • 在内存中编译
  • stats.toJson 加速
  • Devtool
  • 避免在生产环境下才会用到的工具
  • 最小化 entry chunk
  • 避免额外的优化步骤
  • 输出结果不携带路径信息

3. 生产环境

  • Source Maps