webpack入门

87 阅读2分钟

webpack作为每一个前端人都直接或间接使用的前端模块化打包工具,十分有必要去好好学习。

能干什么(大大提高开发效率和灵活性)

  • 让项目支持scss、less、es6、ts等浏览器目前无法处理的功能
  • 丰富脚手架功能(如配置gzip,引用cdn资源,抽取公共代码,自定义loader和plugin实现定制化功能)

如何工作

最常用的方式就是在package.json文件的scripts里添加对应的script,去读取webpack.config.js(可以配置别的文件webpack --config ${fliePath})文件,从配置的入口(entry)开始,一层一层的去寻找所需要的依赖,得到一个完整的依赖关系图,这个依赖关系图会包括当前项目所需要的所有模块,然后根据这个关系,去遍历项目,打包一个个模块(使用不同的loader处理不同的文件类型)

webpack的模块化

  • CommonJS模块化实现原理
// 定义一个以模块路径为键,模块函数为值的对象
var __webpack_modules__ = {
  '${modulePath}': (function (module) {
    const hello = (msg) => {
      return `hello,${msg}`
    }
    //...
    module.exports = {
      hello,
      ...//
    }
  })
}
// 定义一个缓存模块的对象
var __webpack_module_cache__ = {};

// 定义一个加载模块的函数
function __webpack_require__(moduleId) {
  // 1.判断缓存中是否已经加载过
  if (__webpack_module_cache__[moduleId]) {
    return __webpack_module_cache__[moduleId].exports;
  }

  // 2.给module变量和__webpack_module_cache__[moduleId]赋值了同一个对象
  var module = __webpack_module_cache__[moduleId] = { exports: {} };

  // 3.加载执行模块
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

  // 4.导出module.exports
  return module.exports;
}

// 具体开始执行代码逻辑
!function () {
  // 根据modulePath加载
  const { hello, ... } = __webpack_require__("${modulePath}");
  console.log(hello("webpack"));
}();

  • ES Module实现原理
var __webpack_modules__ = {
    "${entryFilePath}":  (function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
          // 使用r函数给模块打标签,设置其__esModule标识
          __webpack_require__.r(__webpack_exports__);

          var _js_hello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("${modulePath}");

          console.log(_js_hello__WEBPACK_IMPORTED_MODULE_0__.hello('webpack'));
    }),
    "${modulePath}":  (function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
          __webpack_require__.r(__webpack_exports__);

          // 调用了d函数: 给exports设置了一个代理definition
          // exports对象中本身是没有对应的函数
          __webpack_require__.d(__webpack_exports__, {
            "hello": function () { return hello; },
            ...
          });

          const hello = (msg) => {
            return `hello,${msg}`
          }
          ...
    })
}

var __webpack_module_cache__ = {};

function __webpack_require__(moduleId) {
  if (__webpack_module_cache__[moduleId]) {
    return __webpack_module_cache__[moduleId].exports;
  }
  var module = __webpack_module_cache__[moduleId] = {
    exports: {}
  };
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  return module.exports;
}

!function () {
  // __webpack_require__这个函数对象添加了一个属性: d -> 值function
  __webpack_require__.d = function (exports, definition) {
    for (var key in definition) {
      if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
        //给exports设置了一个代理,通过传入的definition去获取对应的模块
        Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
      }
    }
  };
}();


!function () {
      // __webpack_require__这个函数对象添加了一个属性: o -> 值function
      __webpack_require__.o = function (obj, prop) { 
          return Object.prototype.hasOwnProperty.call(obj, prop); 
     }
}();

!function () {
  // __webpack_require__这个函数对象添加了一个属性: r -> 值function
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };
}();


__webpack_require__("${entryFilePath}");

出口和入口的配置

const path = require('path')
module.exports = {
    // entry: "./src/index.js", //配置入口文件,webpack会从该文件开始读取对应的文件进行操作
    entry: {
        index: "./src/index.js",
        main: "./src/main.js"
    },
    // [https://webpack.js.org/configuration/output/]
    output: {
        filename: "[name].bundle.js", // 打包生成的文件名
        path: path.resolve(__dirname, "./build"), //必须配置绝对路径
        publicPath: './', //设置打包后文件的前缀
        chunkFilename: "[name].[chunkhash:6].chunk.js"
    }
}

模块热替换Hot Module Replacement

HMR(基于webpack-dev-server)可以让我们边修改文件,对应的模块就产生对应的变化,大大提升开发效率。

// ...
module.exports = {
  // ...
  devServer: {
    // 配置参考(https://webpack.js.org/configuration/dev-server/#root)
    hot: 'only',
    host: '0.0.0.0',
    port: 8888,
    open: true,
    compress: true,
    historyApiFallback: true,
    proxy: {
      //基于http-proxy-middleware实现的代理功能,用于解决开发时请求的跨域问题
      '/api': {
        target: `${serverUrl}`,
        pathRewrite: { '^/api': '' },
        secure: false,
        changeOrigin: true,
      },
    }
  },
}

模块解析(Resolve)配置

const path = require('path');
module.exports = {
  //...
  resolve: {
    alias: {
      '~': path.resolve(__dirname, "./src"),
      "pages": path.resolve(__dirname, "./src/pages")
    },
    extensions: ['.js', '.json', '.jsx', '.ts', '.vue'],
    mainFiles: ['index'],
  },
};

rule的配置

  • test属性:匹配对应的资源,使用正则表达式
  • use属性:对应一个数组[useEntry],useEntry又是一个对象,里面置顶loaderoptions,use里书写的loader一定要遵从从后往前的顺序书写,webpack是依次从后往前使用loader来解析对应test的资源的

常用Loader(用于转换一些特定的类型模块)

style-loader,css-loader,postcss-loader,less-loader(sass-loader)

这一串loader都是处理样式书写的loader,

module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        "style-loader", //将css文件插入到页面中
        {
          loader: "css-loader",
          options: {
            sourceMap: false,
            importLoaders: 2
                      modules: true,
            localIdentName: '[name]_[local]_[hash:base64:5]'
          } // 具体配置查看`https://webpack.js.org/loaders/css-loader/#options`
        },  //解析css文件
        "postcss-loader", // 配合postcss.config.js文件根据browserslist对不同的浏览器处理样式
        "less-loader", // 将less文件转换成css文件
      ]
    }
  ]
}
//postcss.config.js
module.exports = {
  plugins: [
    'postcss-preset-env'
  ]
}
file-loader,url-loader,row-loader到Asset Modules

处理文件解析的loader,webpack4主要是file-loader,url-loader,row-loader三个loader的使用。到了webpack5无需安装解析文件的loader,它内部提供了Asset ModulesAsset Modules | webpack

rules: [
  // webpck4
  {
    test: /\.(png|jpe?g|gif|svg)$/,
    use: [
      {
        loader: 'url-loader',
        options: {
          limit: 4096,
          name: 'img/[name].[hash:8].[ext]'
        }
      }
    ]
  },
  // webpack5
  {
    test: /\.(png|jpe?g|gif|svg)$/,
    type: 'asset',
    paser: {
      dataUrlCondition: {
        maxSize: 4096
      }
    },
    generator: {
      filename: 'img/[name].[hash:8][ext]'
    }
  },
  {
    test: /\.ttf|eot|woff2?$/i,
    type: 'asset/resource',
    generator: {
      filename: 'font/[name].[hash:8][ext]'
    }
  },
  {
    test: /\.svg$/,
    type: 'asset/inline', // 导出 data URI的资源
  },
]
babel-loader

babel是一个javascript编译器。它能将es6的语法做到向后兼容,这样我们就可以放心的使用js的新特性,不用担心语法转换的问题。

rules: [
  {
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
      loader: "babel-loader",
      // 配置建议使用babel.config.js方式
      //option: {
      //    presets: ["babel/preset-env"]
      //}
    },

  }
]
// babel.config.js
module.exports = {
  presets: [
    // https://babeljs.io/docs/en/babel-preset-env
    ["babel/preset-env"],
    ['@babel/preset-react'],
    ["@babel/preset-typescript"]
  ],
  plugins: ['@babel/plugin-proposal-class-properties']
}
vue-loader

vue-loader(它不是一个简单的源转换加载器,它会使用自己的专用加载器链处理每个语言块,最后将这些模块组成最终的模块)配合VueLoaderPlugin解析vue文件

// ...
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.vue$/,
        use: "vue-loader"
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]
}

常用plugin(丰富webpack的功能,如打包优化、环境变量处理、资源管理等)

const path = require('path')
const glob = require('glob')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')
const CopyPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require("terser-webpack-plugin")
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const PurgeCssPlugin = require('purgecss-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer")

const appDir = process.cwd()
const resolveApp = (relativePath) => path.resolve(appDir, relativePath)

module.exports = {
  // ...
  module: {
    rule: [
      // ...
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ]
  },
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        test: /\.js(\?.*)?$/i,
        parallel: true,
        extractComments: false,
        terserOptions: {
          compress: {
            arguments: false,
            dead_code: true
          },
          keep_classnames: true,
          keep_fnames: false,
        },
      }),
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'myApp',
      template: 'public/index.html'
    }),
    new DefinePlugin({
      BASE_URL: '"/manage/"',
      'process.env': {
        NODE_ENV: '"development"',
      }
    }),
    new CopyPlugin({
      patterns: [
        { from: 'source', to: 'dest' },
        {
          from: 'public',
          globOptions: {
            ignore: ['**/index.html', '**/.DS_Store']
          }
        }
      ]
    }),
    new MiniCssExtractPlugin({
      filename: "css/[name].[contenthash:6].css"
    }),
    new CssMinimizerPlugin(),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new PurgeCssPlugin({
      paths: glob.sync(`${resolveApp("./src")}/**/*`, {nodir: true}),
      safelist: function() {
        return {
          standard: ["body", "html"]
        }
      }
    }),
    new CompressionPlugin({
      test: /\.(css|js)$/i,
      algorithm: "gzip",
      threshold: 0,
      minRatio: 0.8      
    }),
    new BundleAnalyzerPlugin()
  ],
}

Code Splitting(代码分离)

  • 利用多入口方式分离
module.exports = {
  entry: {
    main: "./src/main.js",
    index: "./src/index.js"
  },
  output: {
    path: path.resolve(__dirname, "./build"),
    filename: "[name].bundle.js",
  },
}

- 利用SplitChunksPlugin分割代码

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    chunkIds: 'named',
    splitChunks: {
      chunks: 'all',
      miniSize: 2000, // 将包拆分的最小大小
      maxSize: 3000, // 将大于maxSize的包, 拆分成不小于minSize的包
      minChunks: 1,
      maxInitialRequests: 30,
      cacheGroups: {
        // 配置缓存组
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          filename: "js/[id]_vendors.js",
          priority: -10
        },
        default: {
          minChunks: 2,
          filename: "common_[id].js",
          priority: -20
        }
      }
    }
  }
}

Tree Shaking

  • usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化
 optimization: {
    usedExports: true,
    // ...
  },
  • sideEffects:告知webpack某模块是否有副作用

设置CDN优化

  • 将所有静态资源放在CDN服务器上
// 设置output的publibPath
publicPath: `${cdnUrl}`
  • 一些第三方的资源放在CDN服务器上
// 配置 externals将对应的包抽离出来, 然后在模版html上加入对应的cdn资源(对应的cdn资源可以去官网查找)
module.exports = {
  ...
  externals: {
  lodash: "_",
    dayjs: "dayjs"
}
}

常用的开发工具链