likes
comments
collection
share

webpack5从入门到放弃(笔记)

作者站长头像
站长
· 阅读数 9

webpack

什么是webpack

webpack就是一个打包工具,核心宗旨就是一切静态资源皆可打包。

webpack能做什么

  • 将ESModule, CommonJS代码转换为浏览器支持的语法
  • 预编译Less,Sass语言
  • 图片压缩
  • 全局注入依赖文件
  • 提供本地服务器
  • 热更新

基础部分

五大核心概念

  1. entry(入口) 用来指定文件打包的入口文件
  2. output(输出) 用来指定文件的输出路径
  3. loader(加载器) webpack默认只支持js/json文件打包,需要配置loader来解析其他类型资源
  4. plugin(插件) 用来扩展webpack的功能
  5. mode(模式)
  • 开发模式 development
  • 生产模式 production(此模式下,会自动进行代码压缩,预处理等优化)

基础的webpack.config.js格式(在项目根目录下创建此文件)如下

const path = require('path')
module.exports = {
    mode: 'development', // 模式
    entry: './src/main.js', //入口文件
    output: { // 输出目录
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist') // 绝对路径
    },
    module: { // 加载器
        rules: [

        ]
    },
    plugins: [ // 插件

    ]
}

资源模块

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。

在webpack5之前, 通常使用

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

处理样式资源

webpack默认是不能解析css文件,需要添加loader加载器来处理

目前常见的几个样式loader

  1. 常规配置
// webpack.config.js
module.exports = {
    // ...其他配置省略
    module: {
        rules: [
            // loader配置,loader执行顺序从下往上,从右往左
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    }
}
  1. 使用预编译样式语言配置(以scss为列)
  • 安装对应的依赖
npm install sass-loader sass -D
  • 将sass-loader和style-loader进行链式调用
// webpack.config.js
module.exports = {
    // ...其他配置
    module: {
        rules: [
             // loader配置
            {
                test: /\.s[ac]ss$/i,
                use: [
                    'style-loader', // 将js字符串生成style节点
                    'css-loader', // 将css转为commonJS 模块
                    'sass-loader' // 将sass编译为css
                ]
            }
        ]
    }
}

  1. 样式兼容性处理
  • 安装依赖
npm install  postcss-loader postcss postcss-preset-env -D
  • 引入配置(sass-loader之前,css-loader之后使用)
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/i,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  [
                    'postcss-preset-env',
                    {
                      // 其他选项
                    },
                  ],
                ],
              },
            },
          },
        ],
      },
    ],
  },
};
  • package.json配置兼容的级别
// package.json
{
    //...其他配置
    "browserslist": [// 选项之间取交集
        "> 1%", // 市面上99%的浏览器
        "last 2 versions"  // 兼容最近的两个版本
     ],
}

处理图片资源

在webpack5中已经内置了file-loader和url-loader,只需要指定type为'asset'(通用类型)

module.exports = {
    // ... 其他配置
    module: {
        rules: [
            // 加载图片资源
            {
                test: /.(png|jpg|svg|gif|jpeg)$/,
                type: 'asset'
            }
        ]
    }
}
// index.scss
.box2 {
    width: 400px;
    height: 400px;
    background: url('../assets/bg.png')
}
// 打包之后  会在dist文件夹下生成ee6e171b64f6ffb89d31.png,并将scss中的图片URL改为生成的图片路径

默认情况下,asset/resource 模块以 [hash][ext][query] 文件名发送到输出目录。可以通过在 webpack 配置中设置 output.assetModuleFilename 来修改此模板字符串

// webpack.config.js
module.exports = {
    // ...其他配置
    output: {
        filename: 'main.js', // 入口文件的输出路径
        path: path.resolve(__diranme, 'dist'),
        assetModuleFilename: 'images/[hash:8][ext][query]' // hash:8取前8位hash值,ext:后缀名,query:参数
    }
}

另一种自定义输出文件名的方式,通过在加载器中配置generator将某些资源发送到指定目录

module.exports = {
    // ...其他配置
    module: {
        rules: [
             // 加载图片资源
            {
                test: /.(png|jpg|svg|gif|jpeg)$/,
                type: 'asset',
                generator: {// 将资源文件输出到指定目录
                    filename: 'static/images/[hash:8][ext][query]'
                }
            }
        ]
    }
}

处理字体图标

webpack5中默认已经内置file-loader,默认无需配置就能解析字体图标文件,也可以通过设置type来方式来解析

// webpack.config.js
module.exports = {
    // ... 其他配置
    module: {
        rules: [
            {
                test: /.(ttf|woff2?)$/,
                type: 'asset/resource', // 不会转base64  asset根据文件大小会自动转换base64
                generator: {// 将资源文件输出到指定目录
                    filename: 'static/fonts/[hash:8][ext][query]'
                }
            }
        ]
    }
}

其他资源比如音频视频等

// webpack.config.js
module.exports = {
    // ... 其他配置
    module: {
        rules: [
            {
                test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
                type: 'asset/resource',
                generator: {
                    filename: 'static/media/[hash:8][ext][query]'
                }
            }
        ]
    }
}

eslint

可组装的JavaScript和JSX检查工具

基本使用

  • 安装依赖
npm install eslint-webpack-plugin eslint -D
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // ...
  plugins: [new ESLintPlugin({
      context: path.resolve(__dirname, 'src') //指定文件根目录,类型为字符串
  })],
  // ...
};
  • 根目录下创建.eslintrc文件
// .eslintrc.js
module.exports = {
    // 继承eslint规则
    extends: ["eslint:recommended"],
    env: { // 指定你想启用的环境
        node: true, // Node.js 全局变量和 Node.js 作用域
        browser: true, // 浏览器环境中的全局变量。
        es6: true
    },
    parserOptions: {
        sourceType: "module"
    },
    rules: {
        "no-var": 2,
    }
}
  • 配置忽略文件.eslintignore
// .eslintignore
build/*.js
src/assets
public
dist

babel

babel是一个JavaScript编译器,主要用于将 ECMAScript 2015+ 代码转换为当前和旧版浏览器或环境中向后兼容的 JavaScript 版本

  • 转换语法
  • 目标环境中缺少的 Polyfill 功能(通过第三方 polyfill,例如core-js
  • 源码转换

基本使用

  • 安装依赖
npm install -D babel-loader @babel/core @babel/preset-env
  • 配置 loader
module: {
  rules: [
    {
      test: /.js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}
  • 根目录下新建babel.config.js
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  'env': {
    'development': {
      'plugins': ['dynamic-import-node']
    }
  }
}

HtmlWebpackPlugin(简化了 HTML 文件的创建)

该插件将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle

基本使用

  • 安装插件
npm install --save-dev html-webpack-plugin
  • 引入插件
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js',
  },
  plugins: [new HtmlWebpackPlugin({
      // 以public/index.html为模板
      template: path.resolve(__diranme, 'public/index.html')
  })],
};

webpack-dev-server(开发服务器)

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

基本使用

  • 安装依赖
npm install --save-dev webpack-dev-server
  • 引入配置
// webpack.config.js
module.exports = {
    // ...其他配置
    devServer: {
        host: 'localhost',
        port: '3000',
        open: true // 是否启动之后自动打开浏览器
    }
}
  • 启动
webpack server

CopyWebpackPlugin

将已存在的单个文件或整个目录复制到构建目录

基本使用

  • 安装依赖
npm install copy-webpack-plugin --save-dev
  • 引入配置
const CopyPlugin = require("copy-webpack-plugin");
const path = require('path');
module.exports = {
  plugins: [
    new CopyPlugin({
      patterns: [
        { from: path.resolve(__dirname, 'public/*'), to: path.resolve(__dirname, 'dist') },
      ],
    }),
  ],
};

MiniCssExtractPlugin

本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载

与 extract-text-webpack-plugin 相比:

  • 异步加载
  • 没有重复的编译(性能)
  • 更容易使用
  • 特别针对 CSS 开发

基本使用

  • 安装依赖
npm install --save-dev mini-css-extract-plugin
  • 引入插件 建议 mini-css-extract-plugin 与 [css-loader]
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  // ...其他配置
  plugins: [new MiniCssExtractPlugin()],
  module: {
    rules: [
      {
        test: /.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
};

优化

  • 提升开发体验
  • 提升打包构建速度
  • 减少代码体积
  • 优化代码运行性能

SourceMap

SourceMap(源代码映射)是一个将源代码与构建后的代码一一映射的方案

// webpack.config.js
module.exports = {
    // ... 其他配置
    devTool: 'sourceMap' 
}
  • 以下选项非常适合开发环境:

eval - 每个模块都使用 eval() 执行,并且都有 //# sourceURL。此选项会非常快地构建。主要缺点是,由于会映射到转换后的代码,而不是映射到原始代码(没有从 loader 中获取 source map),所以不能正确的显示行数。

eval-source-map - 每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。

eval-cheap-source-map - 类似 eval-source-map,每个模块使用 eval() 执行。这是 "cheap(低开销)" 的 source map,因为它没有生成列映射(column mapping),只是映射行数。它会忽略源自 loader 的 source map,并且仅显示转译后的代码,就像 eval devtool。

eval-cheap-module-source-map - 类似 eval-cheap-source-map,并且,在这种情况下,源自 loader 的 source map 会得到更好的处理结果。然而,loader source map 会被简化为每行一个映射(mapping)。

  • 这些选项通常用于生产环境中:

(none)(省略 devtool 选项) - 不生成 source map。这是一个不错的选择。

source-map - 整个 source map 作为一个单独的文件生成。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它。

hidden-source-map - 与 source-map 相同,但不会为 bundle 添加引用注释。如果你只想 source map 映射那些源自错误报告的错误堆栈跟踪信息,但不想为浏览器开发工具暴露你的 source map,这个选项会很有用。

HMR(热更新)

模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面

  • 保留在完全重新加载页面期间丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

webpack-dev-server默认只支持css的热更新,因为style-loader实现了HMR接口,当HRM接收到更新之后,style-loader会打补丁,并追加到style标签中

webpack-dev-server支持js热更新,需要手动在js文件增加处理

// count.js
import count from './js/count'
import sum from './js/sum'
import './css/index.scss'
import './css/iconfont.css'
console.log(count(2,1))
console.log(sum(1,2,3,4,5,6));

if (module.hot) { // 需要开启热更新
    module.hot.accept('./js/count.js') // 开启指定js文件的热更新
}

如果使用脚手架搭建的项目 vue-loader和react-loader中已经实现了文件的热更新,不需要再做额外处理

include/exclude

通过指定include和exclude来减少loader需要处理的文件

如果include和exclude同时存在, exclude的优先级大于include

cache

  • 开启babel缓存
module.exports = {
    // ... 其他配置
    module: {
        rules: [
            // ...
            {
                test: /.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        cacheDirectory: true, // 开启指定目录缓存babel的结果, 默认缓存路径: node_modules/.cache/babel-loader
                        cacheCompression: false, // 是否对缓存结果进行压缩
                    }
                }
            },
        ]
    }
}
  • eslint开启缓存
module.exports = {
    // ... 其他配置
   plugins: [
       // ...
       new ESLintPlugin({
            context: path.resolve(__dirname, 'src'), //指定文件根目录,类型为字符串
            cache: true, // 开启缓存
            cacheLocation: path.resolve(__dirname, 'node_modules/.cache/eslint/') // 指定缓存的路径, 不指定的话 默认是在根目录下.eslintcache文件中
        }),
   ]
}

Thread

当项目变庞大之后,项目打包速度会越来越慢,而项目中主要耗时的是对js部分的处理,如babel, eslint等操作,此时可以通过thread-loader来开启多个线程来单独处理

使用此loader,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行

在 worker 池中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能生成新的文件。
  • 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
  • 这些 loader 无法获取 webpack 的配置。

每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。(一般只针对耗时的操作进行使用,因为启动线程也是不小的开销)

基本使用

  • 安装依赖
npm install --save-dev thread-loader
  • 依赖配置
module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        include: path.resolve('src'),
        use: [
            {
                loader: "thread-loader",
                // 有同样配置的 loader 会共享一个 worker 池
                options: {
                  // 产生的 worker 的数量,默认是 (cpu 核心数 - 1),或者,
                  // 在 require('os').cpus() 是 undefined 时回退至 1
                  workers: 2,

                  // 一个 worker 进程中并行执行工作的数量
                  // 默认为 20
                  workerParallelJobs: 50,

                  // 额外的 node.js 参数
                  workerNodeArgs: ['--max-old-space-size=1024'],

                  // 允许重新生成一个僵死的 work 池
                  // 这个过程会降低整体编译速度
                  // 并且开发环境应该设置为 false
                  poolRespawn: false,

                  // 闲置时定时删除 worker 进程
                  // 默认为 500(ms)
                  // 可以设置为无穷大,这样在监视模式(--watch)下可以保持 worker 持续存在
                  poolTimeout: 2000,

                  // 池分配给 worker 的工作数量
                  // 默认为 200
                  // 降低这个数值会降低总体的效率,但是会提升工作分布更均一
                  poolParallelJobs: 50,

                  // 池的名称
                  // 可以修改名称来创建其余选项都一样的池
                  name: "my-pool"
                },
              },
          // 耗时的 loader (例如 babel-loader)
        ],
      },
    ],
  },
};

TreeShaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性

webpack5生产模式下自动开启了TreeShaking

针对组件库,或者工具类库,我们只会用到其中部分方法或者组件,未开启TreeShaking时,默认是将所有的内容打包进去,使用只会会去除未引用代码(dead code).

减少babel-loader的辅助代码

Babel 在每个文件都插入了辅助代码,使代码体积过大, babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。

可以引入 Babel runtime 作为一个独立模块,来避免重复引入

基本使用

  • 安装依赖
npm install -D @babel/plugin-transform-runtime @babel/runtime
  • 配置插件
// webpack.config.js
module.exports = {
    // ... 其他配置
    module: {
        rules: [
            // ...
            {
                test: /.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        cacheDirectory: true, // 开启指定目录缓存babel的结果, 默认缓存路径: node_modules/.cache/babel-loader
                        cacheCompression: false, // 是否对缓存结果进行压缩
                        plugins: ['@babel/plugin-transform-runtime'] // 减少代码体积
                    }
                }
            },
        ]
    }
}

图片压缩 - ImageMinimizerWebpackPlugin

  • 安装依赖
npm install image-minimizer-webpack-plugin --save-dev
  • 无损压缩依赖
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo --save-dev
  • 有损压缩依赖
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo --save-dev
  • 配置依赖
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
module.exports = {
    // ...其他配置
    optimization: {
        minimizer: [
            new ImageMinimizerPlugin({ // 压缩图片
                minimizer: {
                  implementation: ImageMinimizerPlugin.imageminGenerate,
                  // Lossless optimization with custom option
                  // Feel free to experiment with options for better result for you
                  options: {
                    plugins: [
                        ["gifsicle", { interlaced: true }],
                        ["jpegtran", { progressive: true }],
                        ["optipng", { optimizationLevel: 5 }],
                        // Svgo configuration here https://github.com/svg/svgo#configuration
                        [
                        "svgo",
                        {
                            plugins: [
                            "preset-default",
                            "prefixIds",
                            {
                                name: "sortAttrs",
                                params: {
                                    xmlnsOrder: "alphabetical"
                                }
                            }
                            ],
                        },
                        ],
                    ],
                  }
                },
              }),
        ]
    },
}

如果出现包下载不下来, 可以尝试通过cnpm 或者梯子进行

CodeSplit代码分割

webpack5中对于动态导入(import()异步导入)的模块会自动分割到单独的文件,但是对于静态导入的模块,则会统一打包到入口文件(main.js)中,会导致入口文件过大,首页加载慢

遇到的问题

  • eslint不认识import()动态导入语法
// .eslintrc.js
module.exports = {
    // ...其他配置
    parserOptions: {
        ecmaVersion: 11 // 指定ecmascript的版本  或者配置成2020
    }
}
  • 动态导入的模块没有生成独立的js文件

检查动态导入的内容是否已经在别的页面进行静态导入,这样会导致动态导入的文件不会生成单独的文件

  • 在开发环境中,动态打包生成的文件名太长,默认是以文件路径为js文件名(src_js_sum_js.js)
// 通过配置webpackChunkName来指定chunk的名称
import(/*webpackChunkName: "sum" */ './js/sum.js').then(res => {
    console.log('res: ', res)
})

配置

从 webpack v4 开始,移除了 CommonsChunkPlugin,取而代之的是 optimization.splitChunks

  • splitChunks默认配置
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 有效值为 `all`,`async`  `initial`。设置为 `all` 可能特别强大,因为这意味着 chunk 可以在异步和非异步 chunk 之间共享
      minSize: 20000, // 生成 chunk 的最小体积(以 bytes 为单位)
      minRemainingSize: 0, // 确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块
      minChunks: 1, // 拆分前必须共享模块的最小 chunks 数。
      maxAsyncRequests: 30, // 按需加载时的最大并行请求数。
      maxInitialRequests: 30, // 入口点的最大并行请求数。
      enforceSizeThreshold: 50000, // 强制执行拆分的体积阈值和其他限制(minRemainingSize,maxAsyncRequests,maxInitialRequests)将被忽略。
      cacheGroups: { // 缓存组可以继承和/或覆盖来自 `splitChunks.*` 的任何选项
        defaultVendors: {
          test: /[\/]node_modules[\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};
  • 常用配置
// webpack.config.js
module.exports = {
    // ...其他配置
    optimization: { // 优化
        splitChunks: { // 代码分割
            chunks: 'all',
            // 分隔组
            cacheGroups: {
                // 抽取第三方模块
                vendors: {
                    test: /node_modules/,
                    priority: -10,
                    name: "vendors",
                    reuseExistingChunk: true,
                },
                // 抽取
                commons: {
                    minSize: 0, // 抽取的chunk最小大小
                    minChunks: 2, // 最小引用数
                    priority: -20,
                    name: "common",
                    reuseExistingChunk: true,
                },
            },
        },
    },
}

预获取/预加载模块(prefetch/preload module)

利用浏览器的空闲时间去下载未来要使用的资源

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。

基本使用

  • 开启预获取prefetch 在动态导入的模块中使用 /* webpackPrefetch: true */
import(/* webpackPrefetch: true */ './js/sum.js').then(res => {
    console.log('res: ', res)
})

打包之后 在index.html中会自动拼上link标签,告知浏览器预获取的资源

<link rel="prefetch" as="script" href="http://localhost:9530/static/js/../../static/js/src_js_sum_js.js">
  • 开启预加载preload 在动态导入的模块中使用 /* webpackPrefetch: true */
import(/* webpackPreload: true */ './js/sum.js').then(res => {
    console.log('res: ', res)
})

扩展