webpack5基础和原理

216 阅读6分钟

前言

如果你还不会webpack,就来一起学习一下吧,本文主要是依据webpack5 来进行学习

基础篇

mode

告诉webpack使用相应模式进行内置优化

  • development
    • 自动会设置 devtool: 'eval'
  • production (默认)

devtool

控制是否生成source map 以及如何生成source map

  • eval (默认)
  • false
  • source-map
  • eval-source-map base64的方式放在eval 后面
  • Inline-source-map 在最下面有一个sourceMappingUrl = base64
  • cheap-source-map。只有行信息,没有列信息
  • cheap-module-source-map(推荐) 对loader 处理的文件展示更加友好
  • hidden-source-map // 有map 文件生成,但是定位不到源代码,需要手动加载到环境下
  • nosource-source-map // 会生成map 文件,但是没有源文件提示

设置规则

(inline | hidden | eval )(nosources)(cheap|cheap-module )sourcemap

source Map

可以根据转换后的代码再次转换为源代码,方便定位调试

当将devtool 设置为source-map 的时候,在打包后,就会额外生成一个main.js.map 文件,格式化后,我们发现里面大致有这几个字段

version: 3   // 版本
sources: []  // 告诉我们将来的map文件是通过哪个源文件转换来的
names: [] // 对names里面的字符进行特殊的处理
mappings: 'xxx' // 类似于映射算法
file: 'x x x' // 源文件名
sourcesContent: [] // 源文件备份
sourceRoot: '' // 记录sourceMap文件的根路径

entry

打包的目录起点

entry: "./src/index.js",

output

  • path : 打包资源输出到哪一个目录

  • filename: 打包输出的文件名

  • publicPath: "", index.html内部引用打包后的js路径,默认是"",

​ 拼接的规则就是拿到前面的域名 + publicPath + filename

output: {
    path: path.resolve(__dirname, "dist"), // 打包的资源输出到哪个目录
    filename: "main[hash:6].js",
    publicPath: "",
    assetModuleFilename: "[name].[hash:6].[ext]", // 打包统一的目录下的名称
  },

resolve

配置当前模块解析规则

resolve: {
    // 配置模块解析规则
    extensions: [".js", ".jsx", ".tsx", ".ts", ".json"],
    alias: { // 配置别名
      "@": path.resolve(__dirname, "src"),
    },
  },

devServer

使用webpack-dev-server 的一系列配置

  • publicPath: 指定本地服务所在的目录,默认值为 / (项目所在目录)
  • contentBase: 打包之后的资源如果依赖了其他没有打包的资源,则告知去哪里找,绝对路径
  • watchContentBase: 和contenBase 配套,进行监听热更新
 devServer: {
    // webpack-dev-server 配置
    // 配置额外的静态文件目录
    contentBase: path.resolve(__dirname, "dist"),
    watchContentBase: true,
    compress: true, // 默认为false, 开启服务端gzip 的压缩
    port: 8080,
    open: true,
    hot: 'only';, // 热更新
    // hotOnly: true, // 后面文档更新为了hot 报错信息不会刷新页面
    publicPath: "/", // 后面文档更新为了static
    // historyApiFallback: true
    proxy: {}
  },

建议output 里面的publicPath 和devServer 里面的publicPath 设置为一样的

proxy

设置代理

 proxy: {
      // http:localhost:8000/api/users
      // https://api.github.com.api/api/users
      "/api": {
        targey: "https://api.github.com", // 实际代理的地址
        pathRewrite: {
          "^/api": "", // 将地址重写,将/api重写为“”, 想当于https://api.github.com.api/users
        },
        changeOrigin: true, // 修改host
      },
    },

loader

loader 是一个模块,将一些文件转换为webpack可以识别的模块,例如css模块不能进行webpack 进行打包,需要进行转换

css-loader 将css文件转换为一个对象,让webpack 能够识别css 语法,style-loader 将转换后的对象应用到页面上展示出style来,利用less-loader 来进行处理less文件(less 会把less 语法转为css)

  • css-loader

  • style-loader

  • less-loader

配置loader 语法

在webpack.config.js

module: {
	rules: [  // 添加多个不同文件的loader
		{
			test: /\.css$/,
			use: [ // 从下往上,从右边往左执行
        {
          loader: "style-loader"// options: ""  暂时不需要参数,所以不传
        },
        {
          loader: 'css-loader'
        }
			]
		},
	  { test: /\.less$/, use: ["style-loader", "css-loader", "less-loader"] }, // 简写方式
	]
}
 module: {
    // loader 分类 pre前置 normal inline post
    rules: [
      { test: /\.txt$/, type: "asset/source" },
      //   { test: /\.css$/, use: ["style-loader", "css-loader"] },
      {
        test: /\.css$/,
        use: [
          {
            loader: "style-loader",
            // options: "",
          },
          {
            loader: "css-loader",
          },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  // require("autoprefixer"),
                  require("postcss-preset-env"),
                ],
              },
            },
          },
        ],
      },
      {
        test: /\.less$/,
        use: [
          "style-loader",
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  // require("autoprefixer"),
                  require("postcss-preset-env"),
                ],
              },
            },
          },
          "less-loader",
        ],
      },
    ],
  },

我们发现在less 和css 中都会用到一样的loader,有一些冗余了,所以可以单独的拿出来一个文件进行配置

项目根目录下创建一个postcss.config.js

module.exports = {
  plugins: [require("postcss-preset-env")],
};

然后model 就可以直接写postcss-loader,就会自动找到他的配置文件postcss.config.js

 {
        test: /\.less$/,
        use: [
          "style-loader",
           {
            loader: "css-loader",
            options: {
              importLoaders: 1,  // 往前找一个,css-loader需要往前找一个loader执行,再次调用postcss-loader
            },
          },
          "postcss-loader",
          "less-loader",
        ]
 }

打包图片 file-loader

将图片也当作模块导入,webpack 默认不能处理,需要用到file-loader

// 引入图片
import oImagesrc from "./img/es.png";
import "./css/img.css";

function packImg() {
  // img 标签,src 属性
  const oEle = document.createElement("div");

  // 创建img标签,设置src
  const oImg = document.createElement("img");
  const requireEsModul = require("./img/es.png");
  console.log(requireEsModul, "---->requireEsModul");
  // file-loader 处理后,会将require 导入后的变为一个对象,里面的defalut属性才是图片地址
  // oImg.src = require("./img/es.png").default;
  
  // 如果file-loader 设置了options属性 esModule: false 则可以直接使用 
  // oImg.src = require("./img/es.png"); 
  
  // 利用import 将图片引入来进行使用
  oImg.src = oImagesrc;
  oEle.appendChild(oImg);

  // 设置背景图片
  const oBgImg = document.createElement("div");
  oBgImg.className = "bgBox";
  oEle.appendChild(oBgImg);

  return oEle;
}

document.body.appendChild(packImg());
  • 使用require 导入图片,如果不给file-loader 配置esmodule: false, 则需要.default 使用
  • 添加esmodule: false
  • 采用import xxx from 图片资源地址

webpack.config.js 配置

 {
        test: /\.(png|svg|gif|jpe?g)$/,
        use: [
          {
            loader: "file-loader",
            options: {
              esModule: false, // 是否转化后的包裹成esModule
              // 修改打包的图片的名称
              /**
               * 占位符号
               * [ext]: 扩展名称
               * [name]:文件名称
               * [hash]: 文件内容产出的hash
               * [contentHash]: hash值
               * [hash:<length>]  截取hash 长度
               * [path]: 路径
               */
              name: "[name].[hash:6].[ext]",
              outputPath: "img",
              // name: "img/[name].[hash:6].[ext]", 简写设置文件打包后的地址以及名称等
            },
          },
        ],
      },

打包图片的时候还有一个是url-loader,使用和file-loader差不多,但是

url-loader 会将图片以base64来进行打包到代码中,减少请求次数,而file-loader则会将图片拷贝到dist 目录下,分开请求,在url-loader 内部也可以调用file-loader. 利用limit 属性

 {
        test: /\.(png|svg|gif|jpe?g)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              esModule: false, 
              name: "img/[name].[hash:6].[ext]",
              limit: 25 * 1024, // 超过就拷贝,没有超过就转base64
            },
          },
        ],
  }

webpack5不需要再去配置file-loader 或者url-loader,他新增了一个asset module type 模块

  • asset/resource == file-loader
  • asset/inline == url-loader
  • asset/source == raw-loader
  • asset. url-loader + limit
// 01 方式一 
{
        test: /\.(png|svg|gif|jpe?g)$/,
        type: "asset/resource",
        // 打包到统一的目录下面 这里也可以在output 下面配置。与这里的区别是,output配置是只要经过asset 都打包到统一的目录下面
        generator: {
          filename: "img/[name].[hash:6][ext]",
        },
  },
    
// 02 方式二
   {
        test: /\.(png|svg|gif|jpe?g)$/,
        type: "asset",
        generator: {
          filename: "img/[name].[hash:6][ext]",
        },
        parser: {
          dataUrlCondition: {  // 相当于limit
            maxSize: 25 * 1024,
          },
        },
    }
  
 output: {
    path: path.resolve(__dirname, "dist"),
    filename: "main.js",
    // 只要经过asset 都打包到统一的目录下面
    assetModuleFilename: "[name].[hash:6][ext]",
  },

asset 处理图标字体资源

自己实现一个loader

browerslistrc 配置

用来配置项目中兼容哪些平台 可以在caniuse.com/中查看(https:/…

安装webpack的时候,已经默认安装配置了,在nodeModules 中的 browserslist这个包,可以利用npx browserslist 来查看当前兼容的浏览器版本

两种配置方法:

  • package.json
"browserlist": [
	">1%",
	"last 2 version",
	"not dead"
]
  • 项目下新建 .browserslistrc
>1%
last 2 version
not dead

postcss

就是通过javascript 来转换样式的工具

比如说给css 补充前缀,做一些css的样式做一些兼容性的处理满足更多的浏览器

npm i postcss posts-cli

  • postcss。相当于一个转换样式的工具
  • postcss-cli. // 安装后,可以在命令行直接使用npx postcss
npx postcss -o ret.css ./src/test.css   // 将test.css 输出到ret.css 中

安装autoprefixer 插件 用来自动安装前缀( autoprefixer.github.io/ 添加前缀)

npx postcss --use autoprefixer -o ret.css ./src/test.css // 将test.css 输出到ret.css 中,添加前缀

如果很多样式都需要添加前缀,所以我们利用postcss-loader 这个loader,在配置文件中设置 npm i postcss-loader

module: {
	rules: [ 
	  { 
      test: /\.css$/, 
      use: [
        	"style-loader", 
        	"css-loader", 
	  	 		{
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [require("autoprefixer")], // 利用插件
              },
            },
          },
	  			] 
    }, 
	]
}
postcss-preset-env 预设(插件的集合)

集合了很多的常见的插件的集合

 {
   loader: "postcss-loader",
   options: {
  	 postcssOptions: {
     plugins: [
       // require("autoprefixer"),
       require("postcss-preset-env"),
     ],
   	},
 },

babel-loader配置

babel 简单了解

安装babel核心包
npm i @babel/core -D

安装脚手架执行babel
@babel/cli -D

安装工具包
@babel/plugin-transform-arrow-function
@babel/plugin-transform-block-scoping

执行使用babel
npx babel src/babeltest.js --out-dir build --plugins=@babel/plugin-transform-arrow-functions


  • npm i babel-loader 安装babel-loader
  • npm i @babel/preset-env 安装预设 或者安装自己需要的plugin
   {
        test: /\.js/,
        exclude: /node_modules/,  // node_modules中的不做处理
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env"], // 预设,就可以不用一个个写plugin
              // 也可以在后面直接配置指定浏览器、
              // presets: [["@babel/preset-env",{ targets: "chrome 91" }]],
              plugins: [],
            },
          },
        ],
      },

babel-loader 在webpack 打包的时候,是会根据.browserslistrc 文件来进行转换的

所以可以把babel-loader 配置文件单独拿出去

  • bable.config.js(json cis mjs)
  • babelrc.json(js)

项目根目录下新建babel.config.js 文件

module.exports = {
  presets: [["@babel/preset-env" /*{ targets: "chrome 91" }*/]],
};

然后webpack.config.js 就可以改为
{
	test: /\.js$/,
	use: ['babel-loader']
}

polyfill

Babel 的预设不一定可以将所有的语法都能转为浏览器兼容可以使用的,

所以当遇到最新的语法的时候,需要使用polyfill

Webpack5 之前需要安装 @babel/polyfill npm i @babel/polyfill --save, 但是打包会消耗更多时间,因为不能按需配置

所以webpack5只要 core-js. 和 regenerator-runtime,不需要@babel/polyfill

npm i core-js regenerator-runtime

然后给babel.config.js 中进行配置

useBuiltIns

  • usage
  • entry
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        // useBuiltIns: false, // 不对当前js处理做polyfill 填充
        useBuiltIns: "usage", // 使用这个后,会根据用户源代码当中所使用的新语法进行填充,corejs 会默认使用2版本,所以需要指定版本corejs 为3
        // useBuiltIns: "entry", // 依据所要兼容的浏览器browserslistrc来进行填充,不会去管源代码用没用,只要浏览器不兼容,就会把它全部填充进来,如果需要按需加载,需要在源代码中添加引入 core-js 和 regenerator-runtime
        // import "core-js/stable"
        // import "regenerator-runtime/runtime"
        corejs: 3,
      },
    ],
  ],
};

ts-loader

 {
   test: /\.ts$/,
   use: ["ts-loader"],
 }

转换ts 文件 ,将ts 文件语法转为javascript, 但是如果文件中用到新特性之类的,还是需要babel-loader 来进行添加polyfill转换,安装**@babel/preset-typescript**


 {
   test: /\.ts$/,
   use: ["babel-loader"],
 },


module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        // useBuiltIns: false, // 不对当前js处理做polyfill 填充
        useBuiltIns: "usage", // 使用这个后,会根据用户源代码当中所使用的新语法进行填充,corejs 会使用2版本,所以需要指定版本corejs 为3
        //  useBuiltIns: "entry", // 依据所要兼容的浏览器browserslistrc来进行填充,不会去管源代码用没用,只要浏览器不兼容,就会把它全部填充进来,如果
        // 需要按需加载,需要在源代码中添加
        corejs: 3,
      },
    ],
    ["@babel/preset-typescript"],
  ],
};

但是使用babel-loader 如果代码有语法错误,在编译阶段,是不会暴露出来的,只会在运行的时候发现,但是使用ts-loader 却可以

这个时候,我们即希望在编译的时候进行语法校验,也希望babel-loader进行polyfill 填充,所以利用pack.json 方式,在build的时候,同时利用tsc检查一个语法

 "tscck": "tsc --noEmit"   // 只会校验语法,但是不会产出新的js 文件
"scripts": {
    "start": "webpack serve",
    "build": "npm run tscck && webpack",
    "tscck": "tsc --noEmit"
  },

plugin

loader 和plugin 区别

loader 对特定的模块类型进行转换, 读取某一个资源类型的内容时候使用

plugin 可以做更多的事情,可以在webpack 打包的任意生命周期中做一些事情

clean-webpack-plugin

清空dist 目录

html-webpack-plugin

配置打包后的index.html

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWbpackPlugin = require("html-webpack-plugin");

webpack.config.js

plugins: [
    new HtmlWbpackPlugin({ template: "./src/index.html" }),
    new CleanWebpackPlugin(),
 ],
definePlugin

webpack 自带的默认的插件,可以使用全局的常量

const { DefinePlugin } = require("webpack");

// 比如配置一个BASE_URL的常量
 plugins: [
    new DefinePlugin({
      BASE_URL: 'lalala', // 注意: 这里会把这个值直接赋值到常量中去
    }),
  ],

copy-webpack-plugin

有时候,public 中不希望webpack进行打包,所以需要进行拷贝

注意: 这里需要9版本,10版本会报错

const CopyWebpackPlugin = require("copy-webpack-plugin");

 new CopyWebpackPlugin({
      patterns: [
        { from: "public" }, // 不写to 的话,会自动复制到配置的output 这个目录
        // { from: "other", to: "public" },
      ],
webpack-dev-server

可以用webpack 打包的时候监听,用vscode 的live server 来进行查看,但是不能局部刷新,也可以在配置文件里面配置watch: true, 同样,有性能问题,

webpack-dev-server 可以实现局部刷新打包

npm i webpack-dev-server

 "start": "webpack serve",
 "build": "webpack"
 "wacth": "webpack --watch"
实现plugin

自己实现一个plugin

webpack.config.js

为了区分生产环境打包配置和开发环境打包配置,我们进行了对webppack.config.js 进行拆分配置。分为三个文件,一个基础文件配置,在生产和开发都会用到,然后一个生产环境配置,一个开发环境配置。通过利用 webpack-merge 将配置文件合并

首先需要修改我们的package.json 文件中的script, 将打包命令执行的webpack配置文件目录通过--config指定为我们想要执行的那个文件,然后通过 --env 来传入当前环境的参数

"scripts": {
    "build": "webpack --config ./config/webpack.common.js --env production",
    "serve": "webpack serve --config ./config/webpack.common.js --env development",
  },

在根目录下新建config 文件夹,里面创建四个文件

  • webpack.common.js
  • webpack.dev.js
  • webpack.pro.js
  • pathUtils.js // 来配置绑定当前上下文路径

因为我们在写配置别名和output的时候,需要传入一个绝对路径,但是目前我们修改完后,webpack的配置文件路径也就对应不上了,所以利用pathUtils.js 里面的方法来保存一个绝对路径地址。

// pathUtils.js

const path = require("path");
// 获取当前运行地址
const currentDir = process.cwd();
// 根据当前根目录生成绝对路径地址
const resolveCurrent = (relativePath) => {
  return path.resolve(currentDir, relativePath);
};
module.exports = resolveCurrent;

然后来配置我们的webpack.common.js 文件,我们把公共的配置都抽离到这个文件中,然后利用 webpack-merge 将配置文件合并

const { merge } = require("webpack-merge");
// 导入确定路径的utils函数
const resolveCurrent = require("./pathalis");
// 导入公共插件
const HtmlWbpackPlugin = require("html-webpack-plugin");



// 导入其他配置文件
// 导入生产环境webpack配置
const prodConfig = require("./webpack.prod");
// 导入开发环境webpack配置
const devConfig = require("./webpack.dev");


// 导出webpack配置文件
// 这里导出一个函数是为了接受package.json里面传入的环境参数
module.exports = (env) => {
  // 根据传入环境变量判断
  const isProduction = env.production;
  // 根据当前打包模式判断要和哪一个配置合并
  const config = isProduction ? prodConfig : devConfig;
	// 利用webpack-merge合并配置文件并返回
  const mergeConfig = merge(baseConfig, config);
  return mergeConfig;
};


// // 定义对象,保存基础配置信息
const baseConfig = {
  entry: "./src/index.js",
  resolve: {
    // 配置模块解析规则
    extensions: [".js", ".jsx", ".tsx", ".ts", ".json"],
    alias: {
      "@": resolveCurrent("./src"),  // 修改路径
    },
  },
  output: {
    path: resolveCurrent("./dist"), // 打包的资源输出到哪个目录
    filename: "main[hash:6].js",
    publicPath: "",
  },

  module: {
    // loader 分类 pre前置 normal inline post
    rules: [
      //   { test: /\.css$/, use: ["style-loader", "css-loader"] },
      {
        test: /\.css$/,
        use: [
          {
            loader: "style-loader",
            // options: "",
          },
          {
            loader: "css-loader",
            options: {
              importLoaders: 1,
            },
          },
          {
            loader: "postcss-loader",
            // options: {
            //   postcssOptions: {
            //     plugins: [
            //       //   require("autoprefixer"),
            //       require("postcss-preset-env"),
            //     ],
            //   },
            // },
          },
        ],
      },
      {
        test: /\.less$/,
        use: [
          "style-loader",
          "css-loader",
          "postcss-loader",
          //   {
          //     loader: "postcss-loader",
          //     options: {
          //       postcssOptions: {
          //         plugins: [
          //           //   require("autoprefixer"),
          //           require("postcss-preset-env"),
          //         ],
          //       },
          //     },
          //   },
          "less-loader",
        ],
      },
      // {
      //   test: /\.(png|svg|gif|jpe?g)$/,
      //   use: [
      //     {
      //       loader: "url-loader",
      //       options: {
      //         esModule: false, // 是否转化后的包裹成esModule
      //         // 修改打包的图片的名称
      //         // 占位符号

      //         //   [ext]: 扩展名称
      //         //   [name]:文件名称
      //         //  [hash]: 文件内容产出的hash
      //         //  [contentHash]: hash值
      //         //  [hash:<length>]  截取hash 长度
      //         //   [path]: 路径

      //         name: "[name].[hash:6].[ext]",
      //         // name: "img/[name].[hash:6].[ext]",
      //         outputPath: "img",
      //         limit: 25 * 1024, // 超过就拷贝,没有超过就转base64
      //       },
      //     },
      //   ],
      // },
      {
        test: /\.(png|svg|gif|jpe?g)$/,
        type: "asset",
        // 只需要经过asset 都打包到统一的目录下面
        generator: {
          filename: "img/[name].[hash:6][ext]",
        },
        parser: {
          dataUrlCondition: {
            maxSize: 25 * 1024,
          },
        },
      },
      {
        test: /\.js/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader",
            // options: {
            //   presets: [["@babel/preset-env" /*{ targets: "chrome 91" }*/]],
            //   plugins: [],
            // },
          },
        ],
      },
      {
        test: /\.ts$/,
        use: ["babel-loader"],
      },
    ],
  },

  plugins: [
    new HtmlWbpackPlugin({
      template: "./src/index.html",
      title: "lalala",
    }),
  ],
};

公共的配置完成后,只需要写入不同环境下的配置就可以了

在webpack.prod.js 中写入生产环境配置

const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  mode: "production",
  devtool: "source-map",
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [
        { from: "public" },
        // { from: "other", to: "public" },
      ],
    }),
  ],
};

同理,在webpack.dev.js中写入开发环境配置

用到的依赖版本,有的依赖在实际项目中其实并不需要安装

"dependencies": {
    "@babel/preset-env": "^7.16.11",
    "@babel/preset-typescript": "^7.16.7",
    "autoprefixer": "^10.4.2",
    "core-js": "^3.21.0",
    "css-loader": "^5.2.6",
    "html-webpack-plugin": "^5.3.1",
    "less": "^4.1.1",
    "less-loader": "^9.0.0",
    "postcss-loader": "^6.2.1",
    "regenerator-runtime": "^0.13.9",
    "style-loader": "^2.0.0",
    "ts-loader": "^9.2.6",
    "webpack": "^5.38.1",
    "webpack-cli": "^4.7.0",
    "webpack-dev-server": "^3.11.2"
  },
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.2",
    "@babel/plugin-transform-arrow-functions": "^7.16.7",
    "babel-loader": "^8.2.3",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^9.0.0",
    "file-loader": "^6.2.0",
    "postcss": "^8.4.6",
    "postcss-cli": "^9.1.0",
    "postcss-preset-env": "^7.3.1",
    "url-loader": "^4.1.1"
  }

基础篇完结

原理篇

手写loader

// TODO:

手写plugin

// TODO:

如有错误,还请指出~~, 看到这里啦,点个👍吧~~