webpack知识点整理

126 阅读9分钟

我们平常使用框架写出来的代码,如.vue, .jsx, .less等文件浏览器是不认识的,需要进行转换,webpack就是干这事的,同时在转换过程中可以进行优化。

webpack的构建流程

  1. 合并shell参数和文件配置的参数
  2. 注册配置的插件,插件会监听 Webpack 构建生命周期的事件节点
  3. 执行run方法开始执行编译
  4. 根据Entry,对每个Module调用对应的Loader,生成AST, 再找到该 Module 依赖的 Module,递归地进行编译处理
  5. 对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统

常用配置项

mode(环境配置)

  1. production 生产模式下,Webpack 会自动优化打包结果;(例如:代码的压缩混淆等)
  2. development 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
  3. none 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;

entry和output

单页面

module.exports = {
    // entry: './app.js', // 字符串
    entry:['./a.js','./b.js'],  // 数组
    output: {
      path: path.resolve(__dirname, './dist'),
      filename: 'index_bundle.js', // 固定
    },
    plugins: [new HtmlWebpackPlugin()],
}

多页面

module.exports = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
  },
  output: {
    filename: '[name].js',  // 输出名称和输入的文件名一致
  },
  plugins: [
    new HtmlWebpackPlugin(
      {
        template: './src/pages/pageOne/index.html',
        chunks: ['pageOne'],
      }
    ),
    new HtmlWebpackPlugin(
      {
        template: './src/pages/pageTwo/index.html',
        chunks: ['pageTwo'],
      }
    ),
 ]
};

多页面时entry一般是不固定的,我们可以读取src下的目录自动生成entry和HtmlWebpackPlugin

const path = require("path");
const glob = require("glob");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const setMPA = () => {
  const entry = {};
  const htmlWebpackPlugins = [];
  const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"));

  entryFiles.forEach((entryFile) => {
    const match = entryFile.match(/src\/(.*)\/index\.js/);
    const pageName = match && match[1];

    entry[pageName] = entryFile;
    htmlWebpackPlugins.push(
      new HtmlWebpackPlugin({
        template: path.join(__dirname, `src/${pageName}/index.html`),
        filename: `${pageName}.html`,
        chunks: [pageName],
        inject: true,
        minify: {
          html5: true,
          collapseWhitespace: true,
          preserveLineBreaks: false,
          minifyCSS: true,
          minifyJS: true,
          removeComments: false,
        },
      })
    );
  });

  return {
    entry,
    htmlWebpackPlugins,
  };
};

const { entry, htmlWebpackPlugins } = setMPA();
module.exports = {
  entry,
  // CleanWebpackPlugin 会自动清空output目录
  plugins: [new CleanWebpackPlugin(), ...getHtmlTemplate()],
};

devServer

webpack-dev-server里 Watch 模式默认开启。

devServer: {
    port: 3000, // 默认为8080
    proxy: { // 设置代理
        '/api': {
            target: 'http://localhost:3000',
            pathRewrite:{ // pathRewrite会以正则的方式去替换我们请求的路径
                "^/api":""
            },
            changeOrigin: true, // 跨域
        }
    },
    hot: true, // 是否开启热跟新
    open: true, // 是否自动打开默认浏览器进行预览,
}

module

js

  1. ts-loader: 将ts转为js
  2. babel-loader: 将es6+语法转换为es5语法
  • @bable/core 作用是把js代码分析成ast(抽象语法树),供其它插件使用
  • @babel/preset-env 推荐使用的预置器
  1. .vue文件需要使用vue-loader解析,react不需要单独配置,babel-loader可以解析jsx

css

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== "production";

module.exports = {
  module: {
    rules: [
      {
        test: /\.(sa|sc|c)ss$/i,
        use: [
          devMode ? "style-loader" : MiniCssExtractPlugin.loader,
          "css-loader",
          "postcss-loader",
          "sass-loader",
        ],
      },
    ],
  },
  plugins: [].concat(devMode ? [] : [new MiniCssExtractPlugin()]),
};
  1. css预处理器: 扩展了CSS语言,增加了变量、Mixin、函数等特性, 将css扩展语言编译成CSS
  2. postcss-loader: 处理CSS文件,比如添加浏览器前缀,压缩CSS,转译最新的css特性。
  3. css-loader: 支持css文件中的@import和url语句,处理css-modules,将CSS转化成字符串并作为CommonJS模块导出
  4. style-loader: 将css-loader的结果以style标签的方式插入dom中
  5. mini-css-extract-plugin: 提取 JS 中引入的 CSS 打包到单独文件中,然后通过标签link添加到头部,支持css module, css按需引入
  6. optimize-css-assets-webpack-plugin: 压缩css代码,webpack在生产模式下会自动开启

推荐:

  1. 可以使用postcss替代css预处理器。
  2. 开发使用style-loader,因为它工作得更快。生产使用mini-css-extract-plugin,css分离,可以并行加载,
  3. 缓存问题: 输出带hash的文件名

Asset Modules

之前我们常用的是:

  1. raw-loader: 将文件作为字符串输出,就是返回JSON.stringify后的内容
  2. file-loader: 复制资源文件并替换访问地址,音视频等资源也可以使用它,如background: url()语法和js直接import一个图片
  3. url-loader: 在file-loader的基础上加了一个data URL的功能。传给url-loader一个限制值,如果处理的文件小于这个值,loader将会把文件转化为base64的data URL输出。大于限制的文件则交给引入的file-loader处理。

webpack5帮我们内置了上面的功能。

  1. asset/source 将资源导出为源码字符串. 之前的 raw-loader 功能.
  2. asset/resource 将资源分割为单独的文件,并导出url,之前的 file-loader的功能.
  3. asset/inline 将资源导出为dataURL(url(data:))的形式,之前的 url-loader的功能.
  4. asset 自动选择导出为单独文件或者 dataURL形式(默认为8KB)
module: {
    rules: [
        {
            test: /\.(png|jpg|gif)$/,
            type: `asset`,
            parser: {
                dataUrlCondition: {
                    maxSize: 12111
                }
            }
        }
    ]
},

DLLPlugin

dll 动态链接库,就是缓存,拿空间换时间。把公共库打包成一个单独的库文件,每次只需打包业务代码就行了,这样就减少了公共打包的那部分时间,整体速度得到了提升。

// webpack --config webpack.dll.js

const webpack = require("webpack");
const path = require("path");
module.exports = {
  entry: {
    react: ["react", "react-dom"],
  },
  output: {
    filename: "dll_[name].js",
    library: "[name]_[hash]",
    path: path.resolve(__dirname, "dist/site"),
  },
  plugins: [
    new webpack.DllPlugin({
      context: __dirname,
      path: path.join(__dirname, 'manifest.json'),
      name: "[name]_[hash]",
    }),
  ],
};

使用

new webpack.DllReferencePlugin({
  context: __dirname,
  manifest: require('./manifest.json'),
});

devtool

选项:

  1. false: 没有映射
  2. source-map: 生成单独的映射文件
  3. inline-: 将映射文件内联到原始文件中
  4. hidden: 会生成映射文件,但浏览器不会加载
  5. nosources-: 只有模块信息和行信息
  6. eval: 通过 eval 包裹每个模块打包后代码以及对应生成的SourceMap,因为 eval 中为字符串形式,所以当源码变动的时候进行字符串处理会提升 rebuild 的速度。但容易受到xss攻击
  7. cheap: 只定位到行,默认是定位到行和列
  8. module: 编译前的代码
module.exports = {
  devtool: 'cheap-module-eval-source-map' // 开发
  devtool: 'cheap-module-source-map'; // 生产
}

VLQ base64解码

optimization(自定义打包策略)

splitChunks 分包

除cacheGroups的配置项都是公共配置,test, priority,reuseExistingChunk这三个配置不能作为公共配置

默认配置

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000, // 体积不足20k,将不会被拆包
      minRemainingSize: 0,
      minChunks: 1, // 被多次引用,但引用次数小于某个值,将不会被拆包
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000, // 大小超过这个值会被强制拆分
      // 上面都是公共配置,对cacheGroups下的每一项都会生效
      cacheGroups: {
        defaultVendors: { 
          test: /[\/]node_modules[\/]/, // 匹配规则
          priority: -10, // 权重
          reuseExistingChunk: true, // 默认会将匹配到的chunk名称进行相连,该项为true时,直接使用已存在的文件,不会修改名称
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

chunks

  1. async: 动态模块打包进该vendor,非动态模块不进行优化打包
  2. initial: 非动态模块打包进该vendor,动态模块优化打包,如果同时存在动态和非动态导入,不会被打包到一个vendor
  3. all: 自动提取所有公共模块到单独 bundle 注:import()可动态加载模块,返回一个Promise。

minimizer(压缩)

js

webpack5在生产模式下,会自动使用terser-webpack-plugin, 压缩js代码

optimization: {
    minimize: true // 控制开发环境也会生效
},

css

optimization: {
    minimizer: [
      `...`, // 继承默认的配置项,如js压缩,自定义会覆盖默认配置
      new CssMinimizerPlugin(),
    ],
},

Tree-shaking(剔除无用代码)

js

Tree Shaking 只支持 ESM 的引入方式,不支持 Common JS 的引入方式。在使用第三方库时注意。
在生产环境下,Webpack 默认会添加 Tree Shaking 的配置,因此只需写一行 mode: 'production' 即可。

const config = {
 mode: 'development',
 optimization: {
  usedExports: true,
}
 
const config = {
 mode: 'production',
};
  1. 生产环境下才需要开启该功能,开发时不需要
  2. usedExports: 对识别出的无用代码做标记
  3. TerserPlugin: 剔除有无用代码标记的代码
  4. package.json下的sideEffects可以控制tree-shaking的生效范围

sideEffects

sideEffects: true // 全部文件都不可使用tree-shaking
sideEffects: false  // 可对全部文件使用tree-shaking, 包括全局的css文件,需要按下面代码进行处理
sideEffects: ['./src/1.js', '*.css'] // 除了数组中的文件,其它文件都可使用tree-shaking

module.exports = {
  // ...
    module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
        sideEffects: true 
      }
    ]
  },
};

css

剔除无用的css需要用到purgecss

plugins: [
  new PurgecssPlugin({
    paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true }),
  }),
]

常见面试题

module,chunk 和 bundle的区别

我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。

为什么代理能跨域

浏览器才有跨域问题,服务器没有,proxy实际上运行了一个本地服务器,帮我们转发请求,主要是用http-proxy-middleware 这个http代理中间件

const express = require('express');
const proxy = require('http-proxy-middleware');

const app = express();

app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true}));
app.listen(3000);

实现一个loader

loader 其实是一个函数,它的参数是匹配文件的源码,返回结果是处理后的源码, 如将var关键词替换为const:

module.exports = function (source) {
    return source.replace(/var/g, 'const')
}

实现一个plugin

const pluginName = "ConsoleLogOnBuildWebpackPlugin";
class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    // 同步
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log("The webpack build process is starting!");
    });

    // 异步需要传入cb参数,并手动执行
    compiler.hooks.emit.tapAsync(pluginName, (compilation, cb) => {
      compilation.assets["copyright.txt"] = {
        source: function () {
          return "copyright by LEE YANG";
        },
        size: function () {
          return 21;
        },
      };
      cb();
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

Compiler和Compilation的区别

Compiler代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation只是代表了一次新的编译,只要文件有改动,compilation就会被重新创建。

compiler上暴露的一些常用的钩子:

comple 在一次新的compilation前执行 made 完成一次Compilation前执行 done 完成一次Compilation后执行 emit 产出文件到output之前执行

webpack chain

webpack chain提供了很多方法,通过链式调用可以方便的修改各个配置项

chainWebpack: (config) => {
  config.entryPoints.clear(); // 清空入口
  config.entry("main").add("./src/main.js"); // 新增入口

  config.output
    .path("dist")
    .filename("[name].[chunkhash].js")
    .chunkFilename("chunks/[name].[chunkhash].js")
    .libraryTarget("umd")
    .library();
};

// 修改loader
module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule("vue")
      .use("vue-loader")
      .loader("vue-loader")
      .tap((options) => {
        // 修改它的选项...
        return options;
      });
  },
};

hrm原理

{
  entry: [
    require.resolve("webpack-dev-server/client") + "?/", // WebpackDevServer 客户端
    require.resolve("webpack/hot/dev-server"), // 监听执行热更新的事件
    // 入口
    paths.appIndexJs,
  ];
}

  1. 启动本地服务器(Webpack-dev-server),通过修改entry注入客户端和热更新操作,通过sockjs建立websocket长连接
  2. 对文件进行监听(Webpack-dev-middleware)
  3. 监听到文件更改,将重新编译后的代码保存在内存中, 通知浏览器进行更新
  4. 客户端并不请求热更新代码,也不执行热更新模块操作,只是通过emit一个webpackHotUpdate消息,将工作转交给webpack/hot/dev-server
  5. webpack/hot/dev-server会向服务器请求检测是否有新的模块更新,有则返回更新列表,通过jsonp请求最新的模块代码,返回的模块代码内容是直接执行 webpackHotUpdateCallback方法进行模块热替换,热更新过程中如出现错误将回退到刷新浏览器。

参考文章: juejin.cn/post/699277…

Tree-shaking原理

tree-shaking的消除原理是依赖于ES6的模块特性。ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析。

如何处理第三方库?

只导入用到的方法

import { cloneDeep } from 'lodash' // 会打包整个lodash 文件。

import cloneDeep from 'lodash/cloneDeep' // 只会打包cloneDeep

使用esm的库

lodash-es,这个包支持Tree-shaking,

webpack4, webpack5区别

  1. webpack5自带压缩,缓存,web worker
  2. 不再为Node.js内置模块自动添加Polyfills
  3. 内置的静态资源构建能力asset/resource
  4. 支持 Top Level Await

webpack优化

分析工具

  1. progress-bar-webpack-plugin:查看编译进度
  2. speed-measure-webpack-plugin:查看编译速度
  3. webpack-bundle-analyzer:打包体积分析

优化方法

  1. 尽量使用新版本的开发工具,包括node, npm, webpack
  2. 加快构建时间: cache,可加快二次构建速度, 多线程打包
  3. 减小打包体积: 压缩代码、分离重复代码、Tree Shaking,按需引入第三方库
  4. 加载速度:按需加载、浏览器缓存、CDN
  5. 其它:Source Map, hrm

谈谈vite, rollup等其它打包工具

rollup

一般用来打包类库,默认只支持esm, 有很多第三方插件,可以支持项目开发,hrm等

常见插件:

  1. @rollup/plugin-node-resolve 查找外部模块
  2. @rollup/plugin-commonjs 将CommonJS转换成 ES2015 模块的
  3. @rollup/plugin-babel 转译你的 ES6/7 代码

webpack

功能强大,完善,常用于完整的项目中,同时带来的问题就是慢,配置复杂

vite

  1. 启动快 webpack需要打包合并后,发给服务器,vite利用浏览器支持esm的特性,不需要打包,当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。

  2. HMR快 当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高

  3. 生产打包使用的还是rollup