摘要:webpack作为一款主流的构建工具,对比后来者Vite虽然存在一些缺点,例如启动慢,配置复杂等。在很多项目中使用依然基于webpack构建,有必要掌握其概念、构建流程和配置方法。
webpack 是一个用于现代JavaScript 应用程序的静态模块打包工具。当webpack处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图,然后将你项目中所需的每一个模块组合成一个或多个bundles,它们均为静态资源,用于展示你的内容。
这里官方给出的的webpack定义涉及如下关键字:
静态模块: 这里的“静态模块”指的是在开发阶段可以直接被Webpack引用的资源,这些资源可以直接被获取并打包进最终的输出文件(如bundle.js)。静态模块可以包括JavaScript代码、CSS样式表、图片和其他类型的文件。Webpack通过处理应用程序,在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块,并生成一个或多个bundle。这些bundle是静态资源,用于展示应用程序的内容。
打包: 是指将多个模块和资源组合成一个或多个bundle(包)的过程。Webpack 通过分析应用程序的依赖图,将所有必要的模块和资源打包成一个或多个文件,这些文件可以被浏览器加载和执行。 其目的是简化部署和优化加载性能。通过打包,可以减少 HTTP 请求的数量,合并多个资源,以及在加载前对资源进行优化(如压缩、合并等)。打包过程是高度可配置的,允许开发者根据需要定制输出的 bundle 结构,包括代码分割(将应用程序分割成多个chunk,按需加载)、优化资源(如压缩 JavaScript 和 CSS)、提取共享依赖等。
依赖图 (dependency graph) 是指在Webpack构建过程中,Webpack会将所有模块及其依赖关系组织成一个图形结构。每当一个文件依赖于另一个文件时,Webpack就会将这两者视为直接依赖关系。其他概念
概念 | 解释 |
入口(entry) | 入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。 |
输出(output) | output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。 |
loader | webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。 |
插件(plugin) | loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量 |
模式(mode) | 通过选择 development, production 或 none 之中的一个,来设置 mode 参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production |
webpack 构建的核心任务是完成内容转化和资源合并。其完整构建流程主要包含以下 3 个阶段:
初始化阶段
初始化参数:从配置文件、配置对象和Shell参数中读取并与默认参数进行合并,组合成最终使用的参数。
创建编译对象:用上一步得到的参数创建 Compiler 对象。
初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等。
构建阶段
开始编译:执行Compiler对象的run方法,创建Compilation对象。
确认编译入口:进入entryOption阶段,读取配置的Entries,递归遍历所有的入口文件,调用 Compilation.addEntry 将入口文件转换为 Dependency 对象。
编译模块(make): 调用 normalModule 中的 build 开启构建,从 entry 文件开始,调用 loader 对模块进行转译处理,然后调用 JS 解释器(acorn)将内容转化为 AST 对象,然后递归分析依赖,依次处理全部文件。
完成模块编译:在上一步处理好所有模块后,得到模块编译产物和依赖关系图。
生成阶段
输出资源(seal):根据入口和模块之间的依赖关系,组装成多个包含多个模块的 Chunk,再把每个 Chunk 转换成一个 Asset 加入到输出列表,这步是可以修改输出内容的最后机会。
写入文件系统(emitAssets):确定好输出内容后,根据配置的 output 将内容写入文件系统。
构建阶段围绕 module 展开,生成阶段则围绕 chunks 展开。经过构建阶段之后,webpack 得到足够的模块内容与模块关系信息,之后通过 Compilation.seal 函数生成最终资源。[1]
webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。
loader本质上是一个函数,将接收到的内容进行转换并返回转换后的结果。通过loader将其他类型的文件转换成有效的webpack modules(如 ESmodule、CommonJS、AMD),这样webpack就可以消费这些模块,并将其添加到依赖关系图中。
常见的loader有(具体用法后文将有介绍):
区别:style-loader负责将样式插入到DOM中,使样式对页面生效。css-loader主要负责处理import、url路径等外部引用。style-loader会先于css-loader执行。
使用 loader 的方式主要有两种:
module.exports = {
module: {
rules: [{ test: /\.ts$/, use: "ts-loader" }],
},
};
import Styles from "style-loader!css-loader?modules!./styles.css";
wepack正常运行必须依赖node环境,在node环境中须使用npm工具管理node中各种依赖的包,所以安装webpack之前需要安装Node.js。
npm i webpack webpack-cli --save-dev
Webpack-CLI则是用于在命令行中执行Webpack相关操作的工具,通常和Webpack一起使用,以便更好地管理和构建前端项目。
Webpack的配置一般要配置common、dev、pro三个环境,配置多个环境。会在不同的环境使用不同的配置文件进行构建。例如,npm run dev命令会使用build/webpack.dev.js 配置文件
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"devBuild": "webpack --config build-optimization/webpack.dev.js",
"dev": "webpack serve --config build/webpack.dev.js",
"build": "webpack --config build-optimization/webpack.prod.js"
},
dev和pro环境通过merge引入common配置文件
//const webpackCommonConf = require('./webpack.common.js')
const { merge } = require('webpack-merge')
注:也可以借助 cross-env (跨平台通用)包命令,设置参数区分环境。但是要注意cross-env 设置的只在Node.js环境生效,前端代码无法访问process.env.NODE_ENV。需要借助Webpack内置的 DefinePlugin插件。在编译时,将前端代码中匹配的变量名,替换为值或表达式。
下面通过webpack 配置对象,展示 Webpack 如何构建应用程序,其基本组成部分如下(逐行功能参考注释):
module.exports = {
entry: path.join(srcPath, 'index'), //配置入口
module: {
rules: [ //开始定义模块处理规则,通过rules数组来配置不同文件类型的加载器。
{ // 对于所有以.js结尾的文件,使用babel-loader进行转译,只处理srcPath目录下的文件,排除node_modules
test: /\.js$/,
loader: ['babel-loader'],
include: srcPath,
exclude: /node_modules/
},
// {
// test: /\.vue$/, // 匹配所有以 .vue 结尾的文件
// loader: ['vue-loader'], // 使用 vue-loader 来处理 Vue 单文件组件
// include: srcPath
// },
// {
// test: /\.css$/,
// // loader 的执行顺序是:从后往前(知识点)
// loader: ['style-loader', 'css-loader']
// },
{ // 对于.css文件,使用style-loader、css-loader和postcss-loader,注意加载器是从后往前执行的
test: /\.css$/,
// loader 的执行顺序是:从后往前
loader: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
},
{ // 处理.less文件,使用style-loader、css-loader和less-loader,同样遵循加载器的执行顺序
test: /\.less$/,
// 增加 'less-loader' ,注意顺序
loader: ['style-loader', 'css-loader', 'less-loader']
}
]
},
plugins: [
// 使用HtmlWebpackPlugin生成index.html文件,自动将打包后的资源插入其中,template指定了模板文件的位置
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'), // 指定了被打包HTML模板文件的位置。
filename: 'index.html' // filename:指定了生成的HTML文件的名称
})
]
}
开发环境Dev
module.exports = smart(webpackCommonConf, { // 使用smart函数与webpackCommonConf进行合并,这样可以继承通用配置
mode: 'development', // 设置Webpack的模式为development,启用适合开发的优化,比如更快的构建速度和更友好的调试信息
module: {
rules: [
// 对于所有以.png、.jpg、.jpeg和.gif结尾的图片文件,使用file-loader来处理。这会将图片文件复制到输出目录,并返回它们的路径
{
test: /\.(png|jpg|jpeg|gif)$/,
use: 'file-loader'
}
]
},
plugins: [
// 使用DefinePlugin来定义全局常量ENV,值为'development',便于在代码中根据环境做不同的处理
new webpack.DefinePlugin({
// window.ENV = 'development' //定义环境
ENV: JSON.stringify('development')
})
],
devServer: { //启动本地服务,需安装webpack-dev-serve
port: 8080, // 配置devServer,指定本地开发服务器的端口为8080
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动gzip压缩
// 设置代理
proxy: { //解决本地和服务端请求的跨域问题
// 将本地/api/xxx代理到localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地/api2/xxx代理到localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': ''
}
}
}
}
})
插件DefinePlugin允许你定义全局常量,这些常量会在编译时被替换。这样可以在代码中使用这些常量,而不需要在运行时进行判断。
生产环境Pro
module.exports = smart(webpackCommonConf, {
mode: 'production',
output: {
filename: 'bundle.[contenthash:8].js', // 打包代码时,加上hash戳
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件url的前缀(如cdn域名),这里暂时用不到
},
module: {
rules: [
// 图片-考虑 base64 编码的情况
{
test: /\.(png|jpg|jpeg|gif)$/,
use: {
loader: 'url-loader',
options: {
// 小于 5kb 的图片用 base64 格式产出
// 否则,依然延用 file-loader 的形式,产出 url 格式
limit: 5 * 1024,
// 打包到 img 目录下
outputPath: '/img1/',
// 设置图片的 cdn 地址(也可以统一在外面的output中设置,那将作用于所有静态资源)
// publicPath: 'http://cdn.abc.com'
}
}
},
]
},
plugins: [
new CleanWebpackPlugin(), // 会默认清空output.path文件夹
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
})
]
})
dev-server
devServer: { // dev 环境
port: 8080,
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动 gzip 压缩
// 设置代理
proxy: {
// 将本地 /api/xxx 代理到 localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地 /api2/xxx 代理到 localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': ''
}
}
}
}
rules: [ // 使用babel解析conmmon.js中处理
{
test: /\.js$/, //验证规则,处理.js结尾的文件
loader: ['babel-loader'],
include: srcPath, //处理的文件目录
exclude: /node_modules/ //排除此文件夹下的文件
},
]
注:配置babel-loader需要配置babellrc文件;
{
"presets": ["@babel/preset-env"], //preset-env中已经包含ES6、ES7、ES8的语法
"plugins": []
}
Babel是一个JavaScript编译器,主要用于将ECMAScript 2015+(ES6+)代码转换为向后兼容的 JavaScript版本,以便在旧版浏览器或环境中运行Babel 的工作原理可以概括为以下几个步骤:
rules: [
{
test: /\.css$/,
// loader 的执行顺序是:从后往前
loader: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
},
{
test: /\.less$/,
// 增加 'less-loader' ,注意顺序
loader: ['style-loader', 'css-loader', 'less-loader']
}
]
注: Loader的解析顺序是从右往左的
postcss-loader处理CSS会抚平浏览器兼容性的问题(需要配置postcss.config.js文件),允许开发者使用 autoprefixer、cssnano 等工具来优化和转换 CSS;
module.exports = {
plugins: [require('autoprefixer')] //autoprefixer是一个增加前缀的插件
}
css-loader是将文件解析为css文件,将 CSS 文件内容作为模块导入到 JavaScript 中。它支持 @import和url() 语句,能够处理 CSS 依赖;
style-loader将CSS作为