webpack的作用是什么?
Webpack 是 JavaScript 程序的静态模块打包器。它的主要作用是将应用程序中的各种资源,如 JavaScript、CSS、图片、字体等,作为模块进行管理,并将它们打包成一个或多个优化后的文件,以便在浏览器中高效加载和运行。
本文将从以下几个 webpack 功能讲解 vue3 环境搭建
- 代码编译(Loader)
- 扩展功能(Plugin)
- 代码分割(splitChunks)
- 开发环境支持(HMR)
Loader
Loader的作用就是将不同格式的文件转译为浏览器可以执行的文件,如.sass/.vue/.tsx等。Loader本质上是一个函数,负责代码的转译,即对接收到的内容进行转换后将转换后的结果返回 配置Loader通过在
modules.rules中以数组的形式配置。
编译vue3代码需要用到主要loader有:
- vue-loader
- babel-loader
- style-loader & css-loader & sass-loader
- file-loader(使用 webpack5 资源模块 asset/resource 代替)
- url-loader(使用 webpack5 资源模块 asset/inline 代替)
- raw-loader(使用 webpack5 资源模块 asset/source 代替)
- thread-loader
- vue-loader
将单文件组件(
SFC) 解析为vue runtime是可识别的组件模块,需配合VueLoaderPlugin插件使用。
// rules
{
test: /\.vue$/, // 只解析 .vue 文件
use: [
// 开启多线程和缓存
'thread-loader',
{
loader: 'vue-loader',
options: {
// 生成使用 ES modules 语法的 JS 模块。
esModule: true,
},
},
],
},
- babel-loader
将ES6的代码转换成ES5版本,注意解析ts文件是需配置
@babel/preset-typescript,配置allExtensions参数可以解析 vue 文件 setup 的 ts 代码
{
test: /\.ts$/,
use: [
// 开启多线程和缓存
'thread-loader',
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
// useBuiltIns: 'usage', // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
// corejs: 2, // 配置使用core-js使用的版本
loose: true, // 使用loose模式
// 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
targets: {
browsers: [
'> 0.1%',
'last 3 versions',
'ie 10',
'ie 11',
],
},
},
],
[
// 解析ts文件
'@babel/preset-typescript',
{
allExtensions: true, // 支持所有文件扩展名,解析.vue等文件
},
],
],
},
},
],
},
- style-loader & css-loader & sass-loader & MiniCssExtractPlugin.loader
style-loader的功能是在DOM里插入一个<style>标签,并且将CSS写入这个标签内。css-loader的功能是解析 @import 和 url() 的语法,然后将 css 拼接为字符串,最后用 ES module 导出。scss-loader的功能是将 Sass 编译成 CSS。MiniCssExtractPlugin插件的作用,就是提取JS中的CSS样式,用link外部引入,减少JS文件的大小,简称CSS样式分离。
这里的
use数组定义了一系列的加载器,它们按照从后往前的顺序执行。首先,sass-loader会将 Sass 编译成 CSS。然后,css-loader会解析 CSS 中的@import和url()为import/require()并解析它们。最后,style-loader会将 CSS 注入到 DOM 中。
// 使用 css-loader 解析 css
{
test: /\.css$/,
use: [
// 开发环境使用style-looader,打包模式抽离css
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
],
},
// 使用 sass-loader 解析 scss
{
test: /\.(scss|sass)$/,
// 只处理 app/pages 目录下的文件
use: [
// 开发环境使用style-looader,打包模式抽离css
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
},
- webpack5 资源模块
asset/resource发送一个单独的文件并导出 URL。之前通过使用file-loader实现。asset/inline导出一个资源的 data URI。之前通过使用url-loader实现。asset/source导出资源的源代码。之前通过使用raw-loader实现。asset会根据文件的类型选择使用asset/resource或asset/inline
// 解析图片文件
{
test: /.(png|jpe?g|gif|svg)$/, // 匹配图片文件
type: 'asset', // type选择asset
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb转base64位
},
},
generator: {
filename: 'static/images/[name].[contenthash:8][ext]', // 文件输出目录和命名
publicPath, // 文件路径
},
},
// 解析字体
{
test: /.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
type: 'asset', // type选择asset
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb转base64位
},
},
generator: {
filename: 'static/fonts/[name].[contenthash:8][ext]', // 文件输出目录和命名
publicPath,
},
},
// 解析媒体文件
{
test: /.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
type: 'asset', // type选择asset
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb转base64位
},
},
generator: {
filename: 'static/media/[name].[contenthash:8][ext]', // 文件输出目录和命名
publicPath,
},
}
Plugin
通过社区丰富的
Plugin可以实现多种强大的功能,例如代码分割、代码混淆、代码压缩、按需加载.....等等。Plugin本质上是一个带有apply(compiler)的函数,基于tapable这个事件流框架来监听webpack构建/打包过程中发布的hooks来通过自定义的逻辑和功能来改变输出结果。 Plugin通过plugins以数组的形式配置。
编译vue3代码需要用到主要Plugin有:
- VueLoaderPlugin
VueLoaderPlugin的主要作用就是对vue不同模块配置不同的loader,在webpack安装插件时,也就是预处理阶段,VueLoaderPlugin的apply会被调用,该方法中拦截了用户自定义的rules属性,加入对 vue 单文件模块处理的规则后,返回调整后的rules列表
- webpack.ProvidePlugin
ProvidePlugin是webpack的内置插件,作用就是不需要import或require就可以在项目中到处使用配置好的变量。简单理解就是自动导入功能。
- webpack.DefinePlugin
DefinePlugin是webpack的内置插件。用来定义全局变量。
- web-webpack-plugin
web 应用需要加载的资源都需要在 webpack 的 entry 里配置,最后输出对应的代码块,但是要让 web 应用运行起来还需要通过 html 加载这些资源放在浏览器里运行,
web-webpack-plugin作用就是为单页面应用输出HTML,性能优于html-webpack-plugin。
- mini-css-extract-plugin
mini-css-extract-plugin插件,结合optimization.splitChunks.cacheGroups配置,可以提取 CSS 公共部分文件,有效利用缓存,非公共部分文件使用inline方式,且可以设置存放路径(通过设置插件的filename和chunkFilename)。
- clean-webpack-plugin
每次打包时删除上次打包的产物, 保证打包目录下的文件都是最新的
- terser-webpack-plugin
使用 terser 来压缩js代码
- css-minimizer-webpack-plugin
压缩CSS代码
- html-webpack-inject-attributes-plugin
添加自定义属性注入标签
- compression-webpack-plugin
生产环境采用
gzip压缩JS和CSS
// 配置插件
plugins: [
// 使用 VueLoaderPlugin 插件,用于解析 vue 文件
// 将定义的规则应用到 vue 文件的解析对应的标签
// 如匹配 .ts 的,解析script标签内容
new VueLoaderPlugin(),
// 把第三方库注入到 window.context 中
new webpack.ProvidePlugin({
Vue: 'vue',
}),
// 用于定义全局常量
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDARTION_MISMATCH_DETAILS__: false,
}),
// 清理dist目录
new CleanWebpackPlugin({
// root: path.resolve(__dirname, '..'),
// 是否打印日志
verbose: true,
}),
// 用于提取 CSS 公共部分文件,有效利用缓存,非公共部分文件使用inline方式
new MiniCssExtractPlugin({
filename: 'css/[name]_[contenthash:8].css', // 输出的文件名
}),
// gzip压缩
new CompressionPlugin({
test: /\.(js|css)$/, // 只生成css,js压缩文件
filename: '[path][base].gz', // 文件命名
algorithm: 'gzip', // 压缩格式,默认是gzip
threshold: 1, // 只有大小大于该值的资源会被处理。默认值是 10k
minRatio: 0.8, // 压缩率,默认值是 0.8
}),
// 浏览器在请求资源时不发送用户的身份凭证
new HtmlWebpackInjectAttributes({
crossorigin: 'anonymous',
}),
// 生成入口文件
...htmlWebpackPluginList,
]
splitChunks
代码分割配置,将代码分割成多个代码块,充分利用浏览器缓存机制,以减少加载时间
- 什么是chunks?
要理解什么
chunks,首先要搞清楚module、chunk、bundle三者的关系。上面说到
webpack的作用是将应用程序中的各种资源,作为模块进行管理,并将它们打包成一个或多个优化后的文件。那么我们可以理解为未处理前的各种资源模块就是module,而打包处理过后的产物就是bundle。对于打包产物
bundle有些情况下,我们觉得太大了。 为了优化性能,比如快速打开首屏,利用缓存等,我们需要对bundle进行以下拆分,对于拆分出来的东西,我们叫它chunk。
// 配置优化,如代码分割,压缩,tree shaking 等
optimization: {
minimizer: [
new CssMinimizerPlugin(), // 压缩css
new TerserPlugin({ // 压缩js
parallel: true, // 开启多线程压缩
terserOptions: {
compress: {
pure_funcs: ['console.log'], // 删除console.log
},
},
}),
],
/**
* 代码分割配置,将代码分割成多个代码块,充分利用浏览器缓存机制,以减少加载时间
*/
splitChunks: {
chunks: 'all', // 分割代码块的范围,包括所有模块
minSize: 20000, // 分割代码块的最小大小,单位为字节
maxAsyncRequests: 10, // 按需加载时的最大并行请求数
maxInitialRequests: 10, // 入口文件的最大并行请求数
cacheGroups: {
// 提取第三方库
vendors: {
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录下的模块
priority: 1, // 优先级,值越大表示优先级越高
name: 'vendors', // 生成的代码块名称
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
enforce: true, // 强制分割
reuseExistingChunk: true, // 重用已存在的代码块
},
// 提取页面公共代码
commons: {
name: 'commons', // 生成的代码块名称
minChunks: 2, // 最少被引用次数
priority: 0, // 优先级
reuseExistingChunk: true, // 重用已存在的代码块
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
},
},
},
}
实现HMR
HMR的有两种实现方式,一种是通过devserver配置和插件HotModuleReplacementPlugin实现,一种是通过在自定义开发服务下,使用插件webpack-dev-middleware和webpack-Hot-middleware配合实现HMR,本文演示后者实现方式。
- webpack-dev-middleware
webpack-dev-middleware是一个容器(wrapper),它可以把webpack处理后的文件传递给一个服务器(server),实现监听文件改动并重新编译。
webpack-dev-server在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。
- webpack-Hot-middleware
是用来进行页面的热重载的,刷新浏览器
import express from 'express';
import path from 'node:path';
import { type Configuration, webpack } from 'webpack';
import devMiddleware from 'webpack-dev-middleware';
import hotMiddleware from 'webpack-hot-middleware';
const DEV_SERVER_CONFIG = {
HOST: '127.0.0.1',
PROP: 9002,
HMR_PATH: '__webpack_hmr',
TIMEOUT: 20000,
};
// 获取当前app根目录
const basePath = path.resolve(__dirname, '../');
// 合并配置
const webpackDevConfig = merge(baseConfig, devConfig);
// 启动服务器
const app = express();
// webpack 配置
const compiler = webpack(webpackDevConfig);
// 指定静态目录
app.use(express.static(path.resolve(__dirname, '../public/dist')));
// 运用 webpack-dev-middleware 中间件,监听文件改动并重新编译
app.use(devMiddleware(compiler, {
// 落地文件, 其他文件打包到内存,入口文件写入磁盘
writeToDisk: filePath => filePath.endsWith('.tpl'),
// 资源文件路径
publicPath: webpackDevConfig.output?.publicPath,
// headers
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
},
// 输出日志
stats: {
colors: true,
},
}));
// 运用 webpack-hot-middleware 中间件,实现热更新
app.use(hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log: () => { },
}));
console.info('构建中...');
const { PROP } = DEV_SERVER_CONFIG;
app.listen(PROP, () => {
console.log('app listening on port', PROP);
});