系统化学习前端之webpack(进阶)

webpack 高级配置

Webpack 发展至今,除了编译资源外,还拓展了许多 loader 和 plugin 来辅助提升项目性能。

写在前面

webpack 功能之强大,令吾等为之惊叹,欢迎挖宝

提升开发体验

SourceMap

使用 webpack 解析 js 和 css 文件,然后输出到指定文件,代码经过编译输出以后,我们在开发过程中是很难定位 bug 在哪个文件,哪一行的。因此,我们需要通过 SourceMap(源代码映射) 来帮助我们定位 bug 位置。其原理:代码构建过程中,会生成 .map 文件,包含源码与编译代码的映射关系,通过映射可以快速定位 bug。

  1. 开发模式配置 SourceMap

    module.exports = {
      mode: "development",
      devtool: "cheap-module-source-map",
    };
    
  2. 生产模式配置 SourceMap

    module.exports = {
      mode: "production",
      devtool: "source-map",
    };
    

    注意: 开发模式配置 SourceMap 值为:cheap-module-source-map,只包含行映射,没有列映射,打包速度快; 生产模式配置:source-map,包含行、列映射,打包速度慢。根据实际情况,可以自行设置devtool 值 ,其他 devtool 配置

提升打包构建速度

HotModuleReplacement(HMR)

webpack 默认会编译打包项目中所有模块,但是我们开发过程中可能只会针对某一模块修改,因此 HMR 帮助我们只编译打包修改后的模块,其他未修改的模块不参与打包,提高打包速度。其原理:开启 HMR ,修改模块是更新模块内容,页面不刷新,关闭 HMR,修改模块是更新整个页面,页面刷新。

  1. 开启 HMR

    module.exports = {
      devServer: {
    	host: "localhost", 
    	port: "3000", 
    	open: true, 
    	hot: true, // 开启HMR功能(只用于开发环境)
      },
    };
    
  2. 配置 loader

    css 通过 style-loader 处理后具备 HMR 功能;vue 通过 vue-loader 处理后具备 HMR 功能;react 通过 react-hot-loader 处理后具备 HMR 功能;js 文件只能通过在入口文件,使用 module.hot.accept('./xx.js') 重新加载文件方式实现 HMR 功能;

oneOf

使用 loader 时,use 可以配置多个loader,多种文件也会配置使用多个 loader。文件经过 loader 需要先验证,后处理。test 正则使得 loader 不处理部分文件, 但是每个文件处理会默认通过所有 loader 验证,因此降低打包构建速度。而 oneOf 类似正则中的贪婪匹配,只匹配一个 loader,一旦验证且 test 通过,不经过其他 loader 。

module.exports = {
	module: {
		rules: [
		  {
			oneOf: [
			  {
				test: /\.css$/,
				use: ["style-loader", "css-loader"],
			  },
			  {
				test: /\.less$/,
				use: ["style-loader", "css-loader", "less-loader"],
			  },
			  {
				test: /\.s[ac]ss$/,
				use: ["style-loader", "css-loader", "sass-loader"],
			  },
			  {
				test: /\.styl$/,
				use: ["style-loader", "css-loader", "stylus-loader"],
			  },
			  {
				test: /\.(png|jpe?g|gif|webp)$/,
				type: "asset",
				parser: {
				  dataUrlCondition: {
					maxSize: 10 * 1024,
				  },
				},
				generator: {
				  filename: "static/imgs/[hash:8][ext][query]",
				},
			  },
			  {
				test: /\.(ttf|woff2?)$/,
				type: "asset/resource",
				generator: {
				  filename: "static/media/[hash:8][ext][query]",
				},
			  },
			  {
				test: /\.js$/,
				exclude: /node_modules/, 
				loader: "babel-loader",
			  },
			],
		  },
		],
	},
}

Include/Exclude

使用 loader 时,可以使用 test 正则匹配,也可以使用 Include 指定,或者 Exclude 排除部分文件;

module.exports = {
	module: {
		rules: [
			{
				oneOf: [
					{
						test: /\.js$/,
						// exclude: /node_modules/,  // 排除node_modules代码不编译
						include: path.resolve(__dirname, "../src") // 只包含src下的 js 文件
						loader: "babel-loader", // 只使用一个 loader,可以不是用 use
					}
				]
			}
		]
	},
	plugins: [
		new ESLintWebpackPlugin({
		  context: path.resolve(__dirname, "../src"),
		  exclude: "node_modules", // 默认值
		}),
	]
}

Cache

js 文件每次编译打包都需要经过 Eslint 检查和 Babel 编译,影响打包构建速度,使用 Cache 缓存之前的检查和编译结果,提升第二次及后续打包构建速度。

module.exports = {
	module: {
		rules: [
			{
				oneOf: [
					{
						test: /\.js$/,
						// exclude: /node_modules/,  // 排除node_modules代码不编译
						loader: "babel-loader", // 只使用一个 loader,可以不是用 use
						options: {
							cacheDirectory: true, // 开启babel编译缓存
							cacheCompression: false, // 缓存文件不要压缩
						}
					}
				]
			}
		]
	},
	plugins: [
		new ESLintWebpackPlugin({
		  context: path.resolve(__dirname, "../src"),
		  exclude: "node_modules", // 默认值
		  cache: true, // 开启缓存
		  cacheLocation: path.resolve(__dirname, "../node_modules/.cache/.eslintcache"), // 缓存目录
		}),
	]
}

Thead

针对较大的项目,可以使用 Thead 开启多进程,提高打包构建速度。之所以针对较大的项目,是因为每个进程启动会损耗大约 600ms 的开销,较大的项目会有所提升,而小项目可能带来多余的开销。

  1. 安装依赖

    npm i thread-loader -D
    
  2. webpack 配置

    const os = require("os");
    const TerserPlugin = require("terser-webpack-plugin");
    
    const threads = os.cpus().length; // 进程数 == CPU核数
    
    module.exports = {
    	module: {
    		rules: [
    			{
    				oneOf: [
    					{
    						test: /\.js$/,
    						include: path.resolve(__dirname, "../src"),
    						use: [
    			              {
    			                loader: "thread-loader", // 开启多进程
    			                options: {
    			                  workers: threads, // 数量
    			                },
    			              },
    			              {
    			                loader: "babel-loader",
    			                options: {
    			                  cacheDirectory: true, // 开启babel编译缓存
    			                },
    			              },
    			            ],
    					}
    				]
    			}
    		]
    	},
    	plugins: [
    		 new ESLintWebpackPlugin({
    	      context: path.resolve(__dirname, "../src"),
    	      exclude: "node_modules",
    	      cache: true,
    	      cacheLocation: path.resolve(__dirname, "../node_modules/.cache/.eslintcache"),
    	      threads, // 开启多进程
    	    }),
    	],
    	optimization: {
    	    minimize: true,
    	    minimizer: [
    	      new CssMinimizerPlugin(), // css压缩同样可以写到optimization.minimizer里面
    	      new TerserPlugin({ // 生产模式会默认开启TerserPlugin,但需要进行其他配置,需要重新写
    	        parallel: threads // 开启多进程
    	      })
    	    ],
    	 },
    }
    

减少代码体积

Tree Shaking

开发中引入函数库或者组件库,实际开发只使用了部分函数和组件,Tree Shaking 使得按需引入,webpack 默认开启 Tree Shaking。

注意: Tree Shaking 依赖 ES Module,使用CommonJS导入无法使用。

Babel

Babel 在编译的过程中,会为每个文件的 js 都插入辅助代码,对于一些公共方法会重复添加辅助代码,增大代码体积。@babel/plugin-transform-runtime 是禁用了 Babel 注入辅助代码,为 js 提供辅助代码外部源,所有 js 文件都通过 @babel/plugin-transform-runtime 引用辅助代码。

  1. 安装依赖

    npm i @babel/plugin-transform-runtime -D
    
  2. webpack 配置

    module.exports = {
    	module: {
    		rules: [
    			{
    				oneOf: [
    					{
    						test: /\.js$/,
    						include: path.resolve(__dirname, "../src"),
    						use: [
    			              {
    			                loader: "thread-loader", // 开启多进程
    			                options: {
    			                  workers: threads, // 数量
    			                },
    			              },
    			              {
    			                loader: "babel-loader",
    			                options: {
    			                  cacheDirectory: true, // 开启babel编译缓存
    			                  cacheCompression: false, // 缓存文件不要压缩
                  				  plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
    			                },
    			              },
    			            ],
    					}
    				]
    			}
    		]
    	},
    }
    

Image Minimizer

项目开发引入图片资源体积较大,可以通过 image-minimizer-webpack-plugin 对图片资源压缩,减少图片体积。

  1. 安装依赖

    • 无损压缩

      npm i image-minimizer-webpack-plugin imagemin -D
      npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
      
    • 有损压缩

      npm i image-minimizer-webpack-plugin imagemin -D
      npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D
      
  2. webpack 配置

    const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
    
    module.exports = {
    	optimization: {
    		minimizer: [
    			new ImageMinimizerPlugin({
    		        minimizer: {
    		          implementation: ImageMinimizerPlugin.imageminGenerate,
    		          options: {
    		            plugins: [
    		              ["gifsicle", { interlaced: true }],
    		              ["jpegtran", { progressive: true }],
    		              ["optipng", { optimizationLevel: 5 }],
    		              [
    		                "svgo",
    		                {
    		                  plugins: [
    		                    "preset-default",
    		                    "prefixIds",
    		                    {
    		                      name: "sortAttrs",
    		                      params: {
    		                        xmlnsOrder: "alphabetical",
    		                      },
    		                    },
    		                  ],
    		                },
    		              ],
    		            ],
    		          },
    		        },
    		      }),
    		]
    	}
    }
    

    注意:由于包安装的原因,可能导致压缩库下载失败,如果编译过程报错:Error: xxxxx.exe问题,执行下一步。

  3. 安装压缩软件

    • jpegtran 官网 下载 jpegtran.exe 配置项目 node_modules\jpegtran-bin\vendor 下;

    • OptiPNG 官网 下载 optipng.exe 配置项目 node_modules\optipng-bin\vendor 下。

优化代码运行性能

Code Split

webpack 编译打包 js 默认会打包到一个文件中,文件体积较大,影响页面加载。因此需要 Code Split 分割 js 文件,按需加载。

  1. 多入口多出口

    module.exports = {
    	entry: {
    		main: "./src/main.js",
    		app: "./src/app.js"
    	},
    	output: {
    		path: path.resolve(__dirname, "dist"),
    		filename: "js/[name].js",
    		clean: true,
    	}
    }
    

    注意: 配置了多个入口,会输出多个 js 文件。

  2. 单入口模块分割

    module.exports = {
    	entry: "./src/main.js",
    	output: {
    	    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    	    filename: "static/js/[name].js", // 入口文件打包输出资源命名方式
    	    chunkFilename: "static/js/[name].chunk.js", // 动态导入输出资源命名方式
    	    assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
    	    clean: true,
    	},
    	module: {
    		rules: [
    			{
    				oneOf: [
    					{
    			            test: /\.(png|jpe?g|gif|svg)$/,
    			            type: "asset",
    			            parser: {
    			              dataUrlCondition: {
    			                maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
    		              },
    		            },
    		          },
    		          {
    		            test: /\.(ttf|woff2?)$/,
    		            type: "asset/resource",
    		          },
    				]
    			}
    		]
    	},
    	plugins: [
    		new MiniCssExtractPlugin({
    	      filename: "static/css/[name].css",
    	      chunkFilename: "static/css/[name].chunk.css",
    	    }),
    	],
    	splitChunks: {
          chunks: "all", // 对所有模块都进行分割
          // 以下是默认值
          // minSize: 20000, // 分割代码最小的大小
          // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
          // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
          // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
          // maxInitialRequests: 30, // 入口js文件最大并行请求数量
          // enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
          // cacheGroups: { // 组,哪些模块要打包到一个组
          //   defaultVendors: { // 组名
          //     test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
          //     priority: -10, // 权重(越大越高)
          //     reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
          //   },
          //   default: { // 其他没有写的配置会使用上面的默认值
          //     minChunks: 2, // 这里的minChunks权重更大
          //     priority: -20,
          //     reuseExistingChunk: true,
          //   },
          // },
        },
    }
    

Preload / Prefetch

Preload / Prefetch 指定浏览器如何加载资源,均只会加载资源,并缓存,不执行资源。Preload 浏览器立即加载资源,只能加载当前页面需要的资源;Prefetch 浏览器在空闲时才开始加载资源,可以加载当前页面及下一个页面需要的资源;但 Preload 相对于 Prefetch 兼容性好一点。

  1. 安装依赖

    npm i @vue/preload-webpack-plugin -D
    
  2. webpack 配置

    const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
    
    module.exports = {
    	plugins: [
    		new PreloadWebpackPlugin({
    		  // rel: 'prefetch' // prefetch兼容性更差
    	      rel: "preload", // preload兼容性更好
    	      as: "script",
    	    }),
    	]
    }
    

Network Cache

缓存,又爱又恨!静态资源可以通过缓存来提高访问速度,但是文件更新以后,文件名未发生变化会导致浏览器直接读取缓存,不会加载新资源。因此通过hash解决文件名更新是优化缓存使用的根本手段。

  1. fullhash

    webpack4 是 hash,每次修改文件,fullhash都会改变,文件缓存失效。

  2. chunkhash

    根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。js 和 css 是同一个入口文件引入,会共享一个 hash 值。

  3. contenthash

    根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。

    module.exports = {
    	output: {
    	    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    	    filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
    	    chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
    	    assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
    	    clean: true,
    	},
    	plugins: [
    		new MiniCssExtractPlugin({
    	      filename: "static/css/[name].[contenthash:8].css",
    	      chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
    	    }),
    	]
    }
    

    注意:当某一文件内容改变,其 contenthash 值改变,那引入该文件的主文件内容也会发生变化,如 import name.xxx.js 会变成 import name.yyy.js。如此,间接导致主文件缓存失效。针对这一问题,可以将 hash 与文件映射保存在单独 runtime 文件中,runtime 文件小,更新开销小。

    修正后

    module.exports = {
    	output: {
    	    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    	    filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
    	    chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
    	    assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
    	    clean: true,
    	},
    	plugins: [
    		new MiniCssExtractPlugin({
    	      filename: "static/css/[name].[contenthash:8].css",
    	      chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
    	    }),
    	],
    	runtimeChunk: {
          name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
        },
    }
    

Core-js

Babel 可以处理 js 文件中 ES6 语法,如箭头函数,解构等,但是async, promise, 数组部分方法 includes 等是不能处理的,使用 core-js 解决兼容性问题。

  1. 安装依赖

    npm i @babel/eslint-parser -D
    npm i core-js
    
  2. eslint 配置(.eslintrc.js)

    module.exports = {
      // 继承 Eslint 规则
      extends: ["eslint:recommended"],
      parser: "@babel/eslint-parser", // 支持最新的 ECMAScript 标准
      env: {
        node: true, // 启用node中全局变量
        browser: true, // 启用浏览器中全局变量
      },
      plugins: ["import"], // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
      parserOptions: {
        ecmaVersion: 6, // es6
        sourceType: "module", // es module
      },
      rules: {
        "no-var": 2, // 不能使用 var 定义变量
      },
    }
    
  3. babel 配置(babel.config.js)

    module.exports = {
      presets: [
        [
          "@babel/preset-env",
          { useBuiltIns: "usage", corejs: { version: "3", proposals: true } }, // 按需加载core-js的polyfill
        ],
      ],
    };
    

PWA

离线访问,PWA应用,内部通过 Service Workers 技术实现。详细查看 渐进式网络应用程序

写在后面

实际项目应用:

React-cli

点击查看代码
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");

const isProduction = process.env.NODE_ENV === "production";

const getStyleLoaders = (preProcessor) => {
  return [
	isProduction ? MiniCssExtractPlugin.loader : "style-loader",
	"css-loader",
	{
	  loader: "postcss-loader",
	  options: {
		postcssOptions: {
		  plugins: [
			"postcss-preset-env",
		  ],
		},
	  },
	},
	preProcessor && {
	  loader: preProcessor,
	  options:
		preProcessor === "less-loader"
		  ? {
			  // antd的自定义主题
			  lessOptions: {
				modifyVars: {
				  // 其他主题色:https://ant.design/docs/react/customize-theme-cn
				  "@primary-color": "#1DA57A", // 全局主色
				},
				javascriptEnabled: true,
			  },
			}
		  : {},
	},
  ].filter(Boolean);
};

module.exports = {
  entry: "./src/main.js",
  output: {
	path: isProduction ? path.resolve(__dirname, "../dist") : undefined,
	filename: isProduction
	  ? "static/js/[name].[contenthash:10].js"
	  : "static/js/[name].js",
	chunkFilename: isProduction
	  ? "static/js/[name].[contenthash:10].chunk.js"
	  : "static/js/[name].chunk.js",
	assetModuleFilename: "static/js/[hash:10][ext][query]",
	clean: true,
  },
  module: {
	rules: [
	  {
		oneOf: [
		  {
			test: /\.css$/,
			use: getStyleLoaders(),
		  },
		  {
			test: /\.less$/,
			use: getStyleLoaders("less-loader"),
		  },
		  {
			test: /\.s[ac]ss$/,
			use: getStyleLoaders("sass-loader"),
		  },
		  {
			test: /\.styl$/,
			use: getStyleLoaders("stylus-loader"),
		  },
		  {
			test: /\.(png|jpe?g|gif|svg)$/,
			type: "asset",
			parser: {
			  dataUrlCondition: {
				maxSize: 10 * 1024,
			  },
			},
		  },
		  {
			test: /\.(ttf|woff2?)$/,
			type: "asset/resource",
		  },
		  {
			test: /\.(jsx|js)$/,
			include: path.resolve(__dirname, "../src"),
			loader: "babel-loader",
			options: {
			  cacheDirectory: true,
			  cacheCompression: false,
			  plugins: [
				// "@babel/plugin-transform-runtime",  // presets中包含了
				!isProduction && "react-refresh/babel",
			  ].filter(Boolean),
			},
		  },
		],
	  },
	],
  },
  plugins: [
	new ESLintWebpackPlugin({
	  extensions: [".js", ".jsx"],
	  context: path.resolve(__dirname, "../src"),
	  exclude: "node_modules",
	  cache: true,
	  cacheLocation: path.resolve(
		__dirname,
		"../node_modules/.cache/.eslintcache"
	  ),
	}),
	new HtmlWebpackPlugin({
	  template: path.resolve(__dirname, "../public/index.html"),
	}),
	isProduction &&
	  new MiniCssExtractPlugin({
		filename: "static/css/[name].[contenthash:10].css",
		chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
	  }),
	!isProduction && new ReactRefreshWebpackPlugin(),
	// 将public下面的资源复制到dist目录去(除了index.html)
	new CopyPlugin({
	  patterns: [
		{
		  from: path.resolve(__dirname, "../public"),
		  to: path.resolve(__dirname, "../dist"),
		  toType: "dir",
		  noErrorOnMissing: true, // 不生成错误
		  globOptions: {
			// 忽略文件
			ignore: ["**/index.html"],
		  },
		  info: {
			// 跳过terser压缩js
			minimized: true,
		  },
		},
	  ],
	}),
  ].filter(Boolean),
  optimization: {
	minimize: isProduction,
	// 压缩的操作
	minimizer: [
	  // 压缩css
	  new CssMinimizerPlugin(),
	  // 压缩js
	  new TerserWebpackPlugin(),
	  // 压缩图片
	  new ImageMinimizerPlugin({
		minimizer: {
		  implementation: ImageMinimizerPlugin.imageminGenerate,
		  options: {
			plugins: [
			  ["gifsicle", { interlaced: true }],
			  ["jpegtran", { progressive: true }],
			  ["optipng", { optimizationLevel: 5 }],
			  [
				"svgo",
				{
				  plugins: [
					"preset-default",
					"prefixIds",
					{
					  name: "sortAttrs",
					  params: {
						xmlnsOrder: "alphabetical",
					  },
					},
				  ],
				},
			  ],
			],
		  },
		},
	  }),
	],
	// 代码分割配置
	splitChunks: {
	  chunks: "all",
	  cacheGroups: {
		// layouts通常是admin项目的主体布局组件,所有路由组件都要使用的
		// 可以单独打包,从而复用
		// 如果项目中没有,请删除
		layouts: {
		  name: "layouts",
		  test: path.resolve(__dirname, "../src/layouts"),
		  priority: 40,
		},
		// 如果项目中使用antd,此时将所有node_modules打包在一起,那么打包输出文件会比较大。
		// 所以我们将node_modules中比较大的模块单独打包,从而并行加载速度更好
		// 如果项目中没有,请删除
		antd: {
		  name: "chunk-antd",
		  test: /[\\/]node_modules[\\/]antd(.*)/,
		  priority: 30,
		},
		// 将react相关的库单独打包,减少node_modules的chunk体积。
		react: {
		  name: "react",
		  test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
		  chunks: "initial",
		  priority: 20,
		},
		libs: {
		  name: "chunk-libs",
		  test: /[\\/]node_modules[\\/]/,
		  priority: 10, // 权重最低,优先考虑前面内容
		  chunks: "initial",
		},
	  },
	},
	runtimeChunk: {
	  name: (entrypoint) => `runtime~${entrypoint.name}`,
	},
  },
  resolve: {
	extensions: [".jsx", ".js", ".json"],
  },
  devServer: {
	open: true,
	host: "localhost",
	port: 3000,
	hot: true,
	compress: true,
	historyApiFallback: true,
  },
  mode: isProduction ? "production" : "development",
  devtool: isProduction ? "source-map" : "cheap-module-source-map",
  performance: false, // 关闭性能分析,提示速度
};

Vue-cli

点击查看代码
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");
const { DefinePlugin } = require("webpack");
const AutoImport = require("unplugin-auto-import/webpack");
const Components = require("unplugin-vue-components/webpack");
const { ElementPlusResolver } = require("unplugin-vue-components/resolvers");
// 需要通过 cross-env 定义环境变量
const isProduction = process.env.NODE_ENV === "production";

const getStyleLoaders = (preProcessor) => {
  return [
	isProduction ? MiniCssExtractPlugin.loader : "vue-style-loader",
	"css-loader",
	{
	  loader: "postcss-loader",
	  options: {
		postcssOptions: {
		  plugins: ["postcss-preset-env"],
		},
	  },
	},
	preProcessor && {
	  loader: preProcessor,
	  options:
		preProcessor === "sass-loader"
		  ? {
			  // 自定义主题:自动引入我们定义的scss文件
			  additionalData: `@use "@/styles/element/index.scss" as *;`,
			}
		  : {},
	},
  ].filter(Boolean);
};

module.exports = {
  entry: "./src/main.js",
  output: {
	path: isProduction ? path.resolve(__dirname, "../dist") : undefined,
	filename: isProduction
	  ? "static/js/[name].[contenthash:10].js"
	  : "static/js/[name].js",
	chunkFilename: isProduction
	  ? "static/js/[name].[contenthash:10].chunk.js"
	  : "static/js/[name].chunk.js",
	assetModuleFilename: "static/js/[hash:10][ext][query]",
	clean: true,
  },
  module: {
	rules: [
	  {
		test: /\.css$/,
		use: getStyleLoaders(),
	  },
	  {
		test: /\.less$/,
		use: getStyleLoaders("less-loader"),
	  },
	  {
		test: /\.s[ac]ss$/,
		use: getStyleLoaders("sass-loader"),
	  },
	  {
		test: /\.styl$/,
		use: getStyleLoaders("stylus-loader"),
	  },
	  {
		test: /\.(png|jpe?g|gif|svg)$/,
		type: "asset",
		parser: {
		  dataUrlCondition: {
			maxSize: 10 * 1024,
		  },
		},
	  },
	  {
		test: /\.(ttf|woff2?)$/,
		type: "asset/resource",
	  },
	  {
		test: /\.(jsx|js)$/,
		include: path.resolve(__dirname, "../src"),
		loader: "babel-loader",
		options: {
		  cacheDirectory: true,
		  cacheCompression: false,
		  plugins: [
			// "@babel/plugin-transform-runtime" // presets中包含了
		  ],
		},
	  },
	  // vue-loader不支持oneOf
	  {
		test: /\.vue$/,
		loader: "vue-loader", // 内部会给vue文件注入HMR功能代码
		options: {
		  // 开启缓存
		  cacheDirectory: path.resolve(
			__dirname,
			"node_modules/.cache/vue-loader"
		  ),
		},
	  },
	],
  },
  plugins: [
	new ESLintWebpackPlugin({
	  context: path.resolve(__dirname, "../src"),
	  exclude: "node_modules",
	  cache: true,
	  cacheLocation: path.resolve(
		__dirname,
		"../node_modules/.cache/.eslintcache"
	  ),
	}),
	new HtmlWebpackPlugin({
	  template: path.resolve(__dirname, "../public/index.html"),
	}),
	new CopyPlugin({
	  patterns: [
		{
		  from: path.resolve(__dirname, "../public"),
		  to: path.resolve(__dirname, "../dist"),
		  toType: "dir",
		  noErrorOnMissing: true,
		  globOptions: {
			ignore: ["**/index.html"],
		  },
		  info: {
			minimized: true,
		  },
		},
	  ],
	}),
	isProduction &&
	  new MiniCssExtractPlugin({
		filename: "static/css/[name].[contenthash:10].css",
		chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
	  }),
	new VueLoaderPlugin(),
	new DefinePlugin({
	  __VUE_OPTIONS_API__: "true",
	  __VUE_PROD_DEVTOOLS__: "false",
	}),
	// 按需加载element-plus组件样式
	AutoImport({
	  resolvers: [ElementPlusResolver()],
	}),
	Components({
	  resolvers: [
		ElementPlusResolver({
		  importStyle: "sass", // 自定义主题
		}),
	  ],
	}),
  ].filter(Boolean),
  optimization: {
	minimize: isProduction,
	// 压缩的操作
	minimizer: [
	  new CssMinimizerPlugin(),
	  new TerserWebpackPlugin(),
	  new ImageMinimizerPlugin({
		minimizer: {
		  implementation: ImageMinimizerPlugin.imageminGenerate,
		  options: {
			plugins: [
			  ["gifsicle", { interlaced: true }],
			  ["jpegtran", { progressive: true }],
			  ["optipng", { optimizationLevel: 5 }],
			  [
				"svgo",
				{
				  plugins: [
					"preset-default",
					"prefixIds",
					{
					  name: "sortAttrs",
					  params: {
						xmlnsOrder: "alphabetical",
					  },
					},
				  ],
				},
			  ],
			],
		  },
		},
	  }),
	],
	splitChunks: {
	  chunks: "all",
	  cacheGroups: {
		// layouts通常是admin项目的主体布局组件,所有路由组件都要使用的
		// 可以单独打包,从而复用
		// 如果项目中没有,请删除
		layouts: {
		  name: "layouts",
		  test: path.resolve(__dirname, "../src/layouts"),
		  priority: 40,
		},
		// 如果项目中使用element-plus,此时将所有node_modules打包在一起,那么打包输出文件会比较大。
		// 所以我们将node_modules中比较大的模块单独打包,从而并行加载速度更好
		// 如果项目中没有,请删除
		elementUI: {
		  name: "chunk-elementPlus",
		  test: /[\\/]node_modules[\\/]_?element-plus(.*)/,
		  priority: 30,
		},
		// 将vue相关的库单独打包,减少node_modules的chunk体积。
		vue: {
		  name: "vue",
		  test: /[\\/]node_modules[\\/]vue(.*)[\\/]/,
		  chunks: "initial",
		  priority: 20,
		},
		libs: {
		  name: "chunk-libs",
		  test: /[\\/]node_modules[\\/]/,
		  priority: 10, // 权重最低,优先考虑前面内容
		  chunks: "initial",
		},
	  },
	},
	runtimeChunk: {
	  name: (entrypoint) => `runtime~${entrypoint.name}`,
	},
  },
  resolve: {
	extensions: [".vue", ".js", ".json"],
	alias: {
	  // 路径别名
	  "@": path.resolve(__dirname, "../src"),
	},
  },
  devServer: {
	open: true,
	host: "localhost",
	port: 3000,
	hot: true,
	compress: true,
	historyApiFallback: true, // 解决vue-router刷新404问题
  },
  mode: isProduction ? "production" : "development",
  devtool: isProduction ? "source-map" : "cheap-module-source-map",
  performance: false,
};
posted @ 2023-02-21 14:39  深巷酒  阅读(19)  评论(0编辑  收藏  举报