Webpack 5 从零搭建现代化前端项目
Created on
引言
在日常开发中,我们经常使用 Vue CLI、Create React App 等官方脚手架。这些工具虽然方便,但如果不了解其内部原理,在需要自定义配置时会感到困难。本文将带你从零开始,使用 Webpack 5 搭建一个现代化的前端项目,让你深入理解前端工程化的每个环节。
技术选型
- 包管理器:pnpm(也可以使用 yarn 或 npm)
- 构建工具:Webpack 5
- 代码规范:ESLint + Prettier
- 编译工具:Babel
- CSS 处理:PostCSS + Sass/Less
- 代码检查:husky + lint-staged
初始化项目
创建项目目录
mkdir webpack5-project
cd webpack5-project
pnpm init
安装核心依赖
# 安装 Webpack 相关
pnpm add -D webpack@latest webpack-cli@latest webpack-merge@latest webpack-dev-server@latest
# 安装构建优化插件
pnpm add -D compression-webpack-plugin@latest terser-webpack-plugin@latest
项目目录结构
webpack5-project/
├── src/
│ ├── pages/
│ │ ├── page1/
│ │ │ ├── index.html
│ │ │ ├── index.js
│ │ │ └── style.css
│ │ └── page2/
│ │ ├── index.html
│ │ ├── index.js
│ │ └── style.css
│ ├── assets/
│ │ ├── images/
│ │ └── fonts/
│ └── utils/
├── dist/
├── config/
│ ├── webpack.common.js
│ ├── webpack.dev.js
│ └── webpack.prod.js
├── .eslintrc.js
├── .prettierrc.js
├── babel.config.js
├── postcss.config.js
└── package.json
开发环境配置
创建 config/webpack.dev.js:
const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");
module.exports = merge(common, {
mode: "development",
// 开发环境使用 eval-cheap-module-source-map,构建速度快
devtool: "eval-cheap-module-source-map",
// 开发服务器配置
devServer: {
static: {
directory: path.join(__dirname, "../dist"),
},
compress: true,
port: 9000,
hot: true,
open: true,
historyApiFallback: true, // 支持 HTML5 History API
client: {
overlay: {
errors: true,
warnings: false,
},
},
},
// 图片和字体配置
module: {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif|webp)$/i,
type: "asset",
generator: {
publicPath: "/",
filename: "images/[name][ext]",
},
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 8KB以下转base64
},
},
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: "asset/resource",
generator: {
publicPath: "/",
filename: "fonts/[name][ext]",
},
},
],
},
// 开发环境优化
optimization: {
runtimeChunk: "single",
moduleIds: "named",
chunkIds: "named",
},
// 性能提示
performance: {
hints: false,
},
});
生产环境配置
创建 config/webpack.prod.js:
const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");
const TerserPlugin = require("terser-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = merge(common, {
mode: "production",
// 生产环境使用 source-map,便于排查问题
devtool: "source-map",
// 输出配置
output: {
path: path.resolve(__dirname, "../dist"),
filename: "js/[name].[contenthash:8].js",
chunkFilename: "js/[name].[contenthash:8].chunk.js",
assetModuleFilename: "assets/[name].[contenthash:8][ext]",
clean: true, // 清理输出目录
},
// 图片和字体配置
module: {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif|webp)$/i,
type: "asset",
generator: {
publicPath: "/dist/",
filename: "images/[name].[contenthash:8][ext]",
},
parser: {
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: "asset/resource",
generator: {
publicPath: "/dist/",
filename: "fonts/[name].[contenthash:8][ext]",
},
},
],
},
// 生产环境优化
optimization: {
minimize: true,
minimizer: [
// 压缩 JavaScript
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true, // 移除 debugger
pure_funcs: ["console.log"], // 移除特定函数
},
format: {
comments: false, // 移除注释
},
},
extractComments: false,
}),
],
// 代码分割
splitChunks: {
chunks: "all",
cacheGroups: {
// 第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
priority: 10,
reuseExistingChunk: true,
},
// 公共代码
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
minSize: 0,
},
},
},
// 运行时代码单独打包
runtimeChunk: {
name: "runtime",
},
},
plugins: [
// Gzip 压缩
new CompressionPlugin({
test: /\.(js|css|html|svg)$/,
algorithm: "gzip",
threshold: 10240, // 大于10KB才压缩
minRatio: 0.8,
deleteOriginalAssets: false,
}),
],
// 性能提示
performance: {
hints: "warning",
maxEntrypointSize: 512000, // 入口文件大小限制
maxAssetSize: 512000, // 资源文件大小限制
},
});
通用配置
创建 config/webpack.common.js:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const ESLintPlugin = require("eslint-webpack-plugin");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const chalk = require("chalk");
const isDevelopment = process.env.NODE_ENV !== "production";
module.exports = {
// 入口配置
entry: {
page1: path.resolve(__dirname, "../src/pages/page1/index.js"),
page2: path.resolve(__dirname, "../src/pages/page2/index.js"),
},
// 输出配置
output: {
path: path.resolve(__dirname, "../dist"),
filename: "js/[name].js",
clean: true,
},
// 模块解析规则
module: {
rules: [
// HTML
{
test: /\.(html|htm)$/i,
loader: "html-loader",
options: {
minimize: !isDevelopment,
},
},
// JavaScript/JSX
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
cacheDirectory: true, // 启用缓存
},
},
},
// CSS
{
test: /\.css$/i,
use: [
isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
importLoaders: 1,
sourceMap: true,
},
},
"postcss-loader",
],
},
// Sass/SCSS
{
test: /\.(sass|scss)$/i,
use: [
isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
importLoaders: 2,
sourceMap: true,
},
},
"postcss-loader",
"sass-loader",
],
},
// Less
{
test: /\.less$/i,
use: [
isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
importLoaders: 2,
sourceMap: true,
},
},
"postcss-loader",
{
loader: "less-loader",
options: {
lessOptions: {
javascriptEnabled: true,
},
},
},
],
},
],
},
// 路径解析配置
resolve: {
alias: {
"@": path.resolve(__dirname, "../src"),
"@assets": path.resolve(__dirname, "../src/assets"),
"@utils": path.resolve(__dirname, "../src/utils"),
},
extensions: [".js", ".jsx", ".json", ".css", ".scss", ".less"],
// 减少模块搜索层级
modules: [path.resolve(__dirname, "../node_modules")],
},
// 插件配置
plugins: [
// 进度条
new ProgressBarPlugin({
format: ` :msg [:bar] ${chalk.green.bold(":percent")} (:elapsed s)`,
clear: false,
}),
// ESLint
new ESLintPlugin({
extensions: ["js", "jsx"],
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
// HTML 生成
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../src/pages/page1/index.html"),
filename: "index.html",
chunks: ["page1"],
inject: "body",
minify: !isDevelopment && {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
},
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../src/pages/page2/index.html"),
filename: "page2.html",
chunks: ["page2"],
inject: "body",
minify: !isDevelopment && {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
},
}),
// CSS 提取
new MiniCssExtractPlugin({
filename: isDevelopment
? "css/[name].css"
: "css/[name].[contenthash:8].css",
chunkFilename: isDevelopment
? "css/[id].css"
: "css/[id].[contenthash:8].css",
}),
],
// 优化配置
optimization: {
minimizer: [
"...", // 保留默认的 minimizer
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
"default",
{
discardComments: { removeAll: true },
},
],
},
}),
],
},
// 缓存配置
cache: {
type: "filesystem",
cacheDirectory: path.resolve(__dirname, "../node_modules/.cache/webpack"),
},
// 统计信息
stats: {
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false,
},
};
ESLint + Prettier 配置
安装依赖
pnpm add -D eslint prettier eslint-config-prettier eslint-plugin-prettier
ESLint 配置
创建 .eslintrc.js:
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:prettier/recommended", // 必须放在最后
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"prettier/prettier": "error",
},
};
Prettier 配置
创建 .prettierrc.js:
module.exports = {
// 一行最多 100 字符
printWidth: 100,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: "as-needed",
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾需要逗号
trailingComma: "es5",
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
bracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: "always",
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: "preserve",
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: "css",
// vue 文件中的 script 和 style 内不用缩进
vueIndentScriptAndStyle: false,
// 换行符使用 lf
endOfLine: "lf",
};
创建 .prettierignore:
dist
node_modules
*.md
Babel 配置
安装依赖
pnpm add -D @babel/core @babel/preset-env babel-loader core-js@3
创建配置文件
创建 babel.config.js:
module.exports = {
presets: [
[
"@babel/preset-env",
{
// 按需引入 polyfill
useBuiltIns: "usage",
corejs: 3,
// 目标浏览器
targets: {
chrome: "87",
firefox: "78",
safari: "14",
edge: "88",
},
},
],
],
plugins: [],
};
PostCSS 配置
安装依赖
pnpm add -D postcss postcss-loader postcss-preset-env autoprefixer
创建配置文件
创建 postcss.config.js:
module.exports = {
plugins: ["postcss-preset-env", "autoprefixer"],
};
创建 .browserslistrc:
> 0.5%
last 2 versions
not dead
Git Hooks 配置
安装依赖
pnpm add -D husky lint-staged
初始化 husky
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
配置 lint-staged
在 package.json 中添加:
{
"lint-staged": {
"*.{js,jsx}": ["eslint --fix", "prettier --write"],
"*.{css,scss,less}": ["prettier --write"],
"*.{html,md,json}": ["prettier --write"]
}
}
package.json 脚本配置
{
"name": "webpack5-project",
"version": "1.0.0",
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
"build:analyze": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js --env analyze",
"lint": "eslint --ext .js,.jsx src",
"lint:fix": "eslint --ext .js,.jsx src --fix",
"format": "prettier --write \"src/**/*.{js,jsx,css,scss,less,html}\"",
"prepare": "husky install"
},
"devDependencies": {
"@babel/core": "^7.23.0",
"@babel/preset-env": "^7.23.0",
"autoprefixer": "^10.4.16",
"babel-loader": "^9.1.3",
"chalk": "^4.1.2",
"compression-webpack-plugin": "^10.0.0",
"core-js": "^3.33.0",
"cross-env": "^7.0.3",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-webpack-plugin": "^4.0.1",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.3",
"husky": "^8.0.3",
"less": "^4.2.0",
"less-loader": "^11.1.3",
"lint-staged": "^15.0.2",
"mini-css-extract-plugin": "^2.7.6",
"postcss": "^8.4.31",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^9.2.0",
"prettier": "^3.0.3",
"progress-bar-webpack-plugin": "^2.1.0",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"terser-webpack-plugin": "^5.3.9",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.10.0"
}
}
打包分析
安装依赖
pnpm add -D webpack-bundle-analyzer
配置分析工具
在 webpack.prod.js 中添加:
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
module.exports = merge(common, {
// ... 其他配置
plugins: [
// 打包分析
process.env.ANALYZE &&
new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: true,
}),
].filter(Boolean),
});
运行分析:
npm run build:analyze
性能优化建议
1. 使用 Tree Shaking
确保 package.json 中设置:
{
"sideEffects": false
}
或指定有副作用的文件:
{
"sideEffects": ["*.css", "*.scss"]
}
2. 使用 DLL 插件(可选)
对于不常变动的第三方库,可以使用 DLL 插件预编译。
3. 多线程构建
pnpm add -D thread-loader
{
test: /\.js$/,
use: [
'thread-loader',
'babel-loader'
]
}
4. 缩小构建目标
module.exports = {
resolve: {
modules: [path.resolve(__dirname, "node_modules")],
extensions: [".js", ".jsx"], // 只包含必要的扩展名
},
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, "src"), // 只编译 src 目录
exclude: /node_modules/,
},
],
},
};
常见问题
1. 开发服务器无法热更新
确保设置了 target: 'web' 在开发配置中。
2. CSS 模块化
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
},
},
},
],
}
3. 环境变量
使用 cross-env 设置跨平台环境变量,或使用 Webpack 的 DefinePlugin:
const webpack = require("webpack");
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.API_URL": JSON.stringify("https://api.example.com"),
}),
];
总结
本文详细介绍了如何使用 Webpack 5 从零搭建一个完整的前端项目,包括:
- ✅ 开发环境和生产环境的配置分离
- ✅ 完整的代码规范工具链(ESLint + Prettier)
- ✅ 现代化的 JavaScript 编译(Babel)
- ✅ CSS 预处理和后处理(Sass/Less + PostCSS)
- ✅ Git 提交前的代码检查(husky + lint-staged)
- ✅ 性能优化(代码分割、压缩、缓存)
- ✅ 打包分析工具
通过这个配置,你可以构建一个高性能、可维护的现代化前端项目。建议根据实际项目需求进行调整和优化。
参考资源: