文档知识来源:抖音 "哲玄前端",《大前端全栈实践课》
webpack搭建过程
lua
|
Webpack
|
| webpack.base.js ---基础的一些配置
| webpack.dev.js ----本地开发的一些配置
| webpack.prod.js ----生产环境的一些配置
|
dev.js
prod.js
Webpack 工程化常用配置和工具
1. 基本配置文件(webpack.base.js
)
Webpack 的构建配置文件通常包括以下几个部分:
- 入口(Entry) :指定应用的入口文件,Webpack 将从这个文件开始,分析依赖关系并打包。
- 输出(Output) :定义打包后的文件输出路径和文件名。
- 模块加载(Modules) :配置如何处理不同类型的文件(使用 loaders 处理,例如:JS 文件、CSS 文件、图片等)。
- 插件(Plugins) :使用插件对构建过程进行增强(例如:压缩代码、生成 HTML 文件、提取 CSS 等)。
- 模式(Mode) :定义构建模式,如开发模式(
development
)和生产模式(production
),每种模式下,Webpack 会自动应用不同的优化。
2. 常见 Loader 和 Plugin
-
Loaders:用于处理文件,转换文件内容,使其能够被 Webpack 理解和打包。
babel-loader
:用于将 ES6/ES7+ 转换为兼容浏览器的 JavaScript。css-loader
:用于处理 CSS 文件,将其转换为 JavaScript 模块。style-loader
:将 CSS 插入到 DOM 中。file-loader
、url-loader
:用于处理图像和字体等文件,打包成 URL 或将文件嵌入到代码中。
-
Plugins:插件扩展了 Webpack 的功能,帮助优化构建结果。
HtmlWebpackPlugin
:用于生成 HTML 文件,并自动将打包后的 JS 文件注入到 HTML 中。MiniCssExtractPlugin
:提取 CSS 到独立的文件中,防止将 CSS 嵌入到 JavaScript 中。TerserWebpackPlugin
:用于压缩和优化 JavaScript 代码。CleanWebpackPlugin
:清理构建输出目录,删除旧的文件。
3. 代码分割(Code Splitting)
Webpack 提供了 代码分割功能,将大文件拆分成多个小文件,以实现更高效的加载。常见的代码分割方式包括:
- 入口点分割:根据入口点拆分不同的文件。
- 按需加载(Lazy Loading) :动态加载模块,仅在需要时才加载相关代码。
- 公共模块提取:提取多个模块之间共享的代码,减少重复加载。
借鉴一位大佬的理解:
要理解什么
chunks
,首先要搞清楚module
、chunk
、bundle
三者的关系。
webpack
的作用是将应用程序中的各种资源,作为模块进行管理,并将它们打包成一个或多个优化后的文件。那么我们可以理解为未处理前的各种资源模块就是module
,而打包处理过后的产物就是bundle
。对于打包产物
bundle
有些情况下,我们觉得太大了。 为了优化性能,比如快速打开首屏,利用缓存等,我们需要对bundle
进行以下拆分,对于拆分出来的东西,我们叫它chunk
。
具体代码--webpack.base.js---splitChunks
js
optimization: {
/**
* 哲哥分包策略
* 把 js 文件打包成3种类型
* 1. vendor : 第三方 lib ,基本不会动,除非依赖版本升级
* 2. common : 业务组件代码的公共部分抽取出来,改动较少
* 3. entry.{page}: 不同页面 entry 里的业务组件代码的差异部分,会经常改动
* 目的:把改动和引用评率不一样的 js 区分出来,已达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: 'all',// 分割代码块的范围,对同步和异步模块都进行分割,
maxAsyncRequests: 10, // 每次异步加载的最大进行请求数,单位为字节
maxInitialRequests: 10,// 入口点的最大并行请求数
cacheGroups: {
vendor: { // 第三方依赖库
test:/[\\/]node_modules[\\/]/, // 打包node_module 中的文件
name: 'vendor', // 模块名称
priority: 20, // 优先级,数字越大,优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true,//复用已有的公共模块chunk
},
common: { //公共模块
name: 'common', // 模块名称
minChunks: 2 ,// 被两处引用 即被归为公共模块
minSize: 1,// 最小分割文件大小(1 byte)
priority: 10,// 优先级
reuseExistingChunk: true,// 复用已有的公共 chunk
}
}
},
4. Tree Shaking
Webpack 通过 Tree Shaking 特性,能够删除 JavaScript 中未使用的代码,只保留有效部分。这是 Webpack 在生产环境中进行优化的重要手段,能够大幅减小最终输出文件的大小。
5. 热更新(Hot Module Replacement,HMR)
通过 Webpack 提供的 HMR,开发过程中,当代码发生变化时,页面无需重新加载,就能直接更新相应的模块,提高开发效率。
热更新是指在开发环境中,当代码发生变化(如保存文件)时,浏览器可以在不刷新整个页面的情况下,动态地更新变化的模块
当用户在开发保存时,可以被服务器监听的到;【Webpack 使用 webpack-dev-middleware 和 webpack-hot-middleware 实现对文件变化的监听。】
监听到之后,通知解析引擎(编译--模块打包--压缩优化),【文件变化后,Webpack 的解析引擎会重新编译和打包,只处理变化的模块,而不是重新打包整个应用。】
产出落地的 --具体文件的tpl,其他内容放到内存里面,这里的内容就会有变化,【在开发环境下,Webpack 使用内存存储编译结果,避免频繁的磁盘 I/O,提升速度。】
这时,就会去通知浏览器,这边有更新;【webpack-hot-middleware 会通过 WebSocket 或 HTTP 通信的方式,通知浏览器有更新内容。】
然后浏览器就会通过http方式, 把这些东西(js/css)重新请求过来,然后重新刷新页面;【浏览器接收到通知后,动态加载变化的模块(例如更新的 JS/CSS),实现部分刷新,而无需重新加载整个页面。】
express---webpack-dev-middleware --时刻监听着我在本地业务代码的变化,一旦变化,就会通知
express---webpack-hot-middleware--监听内存js/css是否有更新,一旦有更新就会通知浏览器
浏览器响应---(配套的与上面webpack-hot-middleware)浏览器就会接收
【HotModuleReplacementPlugin()】---HMR客户端
6. 缓存优化
Webpack 提供了缓存优化的机制,通过合理配置文件名哈希值(例如:[contenthash]
)来确保文件在更改时能够更新版本,避免浏览器缓存问题。
webpack 代码
下面是webpack相关配置的代码
webpack.base.js
入口entry,模块解析配置module,输出的路径output,配置一些插件plugins,打包的优化optimization
js
const glob = require('glob');
const path = require('path');
const webpack = require('webpack');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
//动态构造 pageEntries HtmlWebpackPluginList
const pageEntries = {};
const HtmlWebpackPluginList = [];
// 获取 app/pages 目录下所有入口文件(entry.xxx.js)
const entryList = path.resolve(process.cwd(),'./app/pages/**/entry.*.js');
glob.sync(entryList).forEach(file => {
const entryName = path.basename(file,'.js');
// console.log('ddd',entryName,file);// entry.page1 /Users/apple/Desktop/webnew/elips/app/pages/page1/entry.page1.js
// 构造 entry
pageEntries[entryName] = file;
// 构造最终渲染的页面文件
HtmlWebpackPluginList.push(
// html-webpackplugin 辅助注入打包后的 bundle 文件到tpl文件中
new HtmlWebpackPlugin({
// 产物(最终模板) 输出路径
filename: path.resolve(process.cwd(),'./app/public/dist/',`${entryName}.tpl`),
// 指定要使用的模板文件
template: path.resolve(process.cwd(),'./app/view/entry.tpl'),
// 要注入的代码快(这里需要和entry里的对应)
chunks: [entryName],
})
)
});
/**
* webpack 基础配置
*/
module.exports = {
// 入口配置--->单页面,一个入口--entry: 'index',
// 多入口配置
entry: pageEntries,
// 模块解析配置(决定了要加载解释哪些模块,以及用什么方式去解释)--各种loader,例如css-loader/babel-loader
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
},{
test: /\.js$/,
include:[
//只对业务代码进行 babel, 加快 webpack 打包速度
path.resolve(process.cwd(),'./app/pages')
],
use: {
loader: 'babel-loader'
},
},{
test: /\.(png|jpe?g|gif)(\?.+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 300,
esModule: false
}
}
},{
test: /\.css$/,
use: ['style-loader','css-loader'],
},{
test: /\.less$/,
use: ['style-loader','css-loader','less-loader'],
},{
test: /\.(eoy|svg|ttf|woff|woff2)(\?\S*)?$/,
use: 'file-loader'
}
]
},
// 产出输出路径,因为开发和生产环境不一致,所以需要再各自的文件里面进行配置
output: {},
// 配置模块解析的具体行为(定义webpack 在打包时,如何找到并解析具体模块的路径)
//例如 imprort {xxxx} from './app/pages/xxx/xxx.js'或者//imprort {xxxx} from '../xxx.vue' ,找这种文件的后缀的一种配置
// 配置了alias之后,import {xxx} from '$pages/xxx/xxx.js'
resolve: {
extensions: ['.js','.vue','.less','.css'],
alias:{
$pages: path.resolve(process.cwd(),'./app/pages'),
$common: path.resolve(process.cwd(),'./app/pages/common'),
$wiggets: path.resolve(process.cwd(),'./app/pages/wiggets'),
$store: path.resolve(process.cwd(),'./app/pages/store'),
},//别名
},
// 配置 webpack 插件
plugins: [
// 处理 .vue 文件,这个插件是必须的
// 它的只能是将你定义过的其他规则复制并应用到 .vue 文件里
// 例如,如果有一条匹配规则 /\.js/ 的规则,那么他会应用到 .vue 文件中的 <script> 模板中
new VueLoaderPlugin(),
// 第三方组件 把第三方库暴露到 window context 下
new webpack.ProvidePlugin({
Vue: 'vue',
axios: 'axios',
_: 'lodash'
}),
// 定义全局常量
new webpack.DefinePlugin({
_VUE_OPTIONS_API_: 'true',// 支持 vue 解析 optionsApi
_VUE_PROD_DECTOLLS_: 'false',// 禁用 Vue 调试工具
_VUE_PROD_HYORATION_MISMATCH_DETAILS_: 'false' ,// 禁用生产环境显示 "水合"信息
}),
// 构造最终渲染的页面模板
/**
* 工程化打包的过程中,
* 1. path.resolve(process.cwd(),'./app/view/entry.tpl') 找到这个tpl
* 2. 找到 chunks: [entry.page2]的代码块,将 entry.page2 的代码块注入到 root 的 vue 的模板里面
* 3. 将注入好的产物,吐出到 filename: path.resolve(process.cwd(),'./app/public/dist/','entry.page2.tpl'), 这个路径下 ,文件名为 entry.page2.tpl
*/
...HtmlWebpackPluginList
],
// 配置打包输出优化(代码分割,模块合并,缓存,TreeShaing,压缩等优化策略)
// 为何要分包,是因为 如果多个文件(A、B)引用了同一个文件(C)内容是,这时打包只会是在A的里面有C,和B的里面有C,这样导致C重复
// 所以有了分包的概念,也就是把相同的内容就行提炼
optimization: {
// 在做分包,对产出的文件进行分包-splitChunks 这个的配置,是有你自己可以配置的
/**
* 哲哥分包策略
* 把 js 文件打包成3种类型
* 1. vendor : 第三方 lib ,基本不会动,除非依赖版本升级
* 2. common : 业务组件代码的公共部分抽取出来,改动较少
* 3. entry.{page}: 不同页面 entry 里的业务组件代码的差异部分,会经常改动
* 目的:把改动和引用评率不一样的 js 区分出来,已达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: 'all',// 对同步和异步模块都进行分割
maxAsyncRequests: 10, // 每次异步加载的最大进行请求书
maxInitialRequests: 10,// 入口点的最大并行请求数
cacheGroups: {
vendor: { // 第三方依赖库
test:/[\\/]node_modules[\\/]/, // 打包node_module 中的文件
name: 'vendor', // 模块名称
priority: 20, // 优先级,数字越大,优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true,//复用已有的公共模块chunk
},
common: { //公共模块
name: 'common', // 模块名称
minChunks: 2 ,// 被两处引用 即被归为公共模块
minSize: 1,// 最小分割文件大小(1 byte)
priority: 10,// 优先级
reuseExistingChunk: true,// 复用已有的公共 chunk
}
}
},
// 将webpack 运行时生成的代码打包到 runtime.js
runtimeChunk: true,
},
}
webpack.dev.js
本地环境内的配置 mode,detoool(便于开发环境调试代码),开发环境输出路径,开发阶段的插件(热更新/开发环境需要一直保存运行的状态)
js
const path = require('path');
const merge = require('webpack-merge');
const webpack = require('webpack');
// 基类配置
const baseConfig = require('./webpack.base.js');
//dev-server 配置
const DEV_SERVER_CONFIG = {
HOST: '127.0.0.1',
PORT: 9002,
HMR_PATH: '__webpack_hmr',//官方规定
TIMEOUT: 20000
};
// 开发阶段的 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach(v =>{
// 第三方包不作为 hmr入口
if(v !== 'vendor'){
baseConfig.entry[v] = [
// 主入口文件
baseConfig.entry[v],
// Hmr 更新入口,官方指定的 hmr 路径
// 这里做的就是HMR客户端(注入代码),也就是浏览器和webpack-hot-middleware,需要建立连接
`webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`
]
}
})
// 本地环境 webpack 配置
const webpackConfig = merge.smart(baseConfig,{
// 指定开发环境模式
mode: 'development',
// source-map 开发工具,呈现代码的映射关系,便于在开发过程中调式代码
devtool: 'eval-cheap-module-source-map',
// 开发环境的output配置
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.resolve(process.cwd(),'./app/public/dist/dev/'), // 输入文件存储路径
publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`,// 外部资源公共路径引用
globalObject: 'this', // 指定全局对象为 `this`,使代码在不同的执行环境下(如浏览器和 Node.js)都能正确工作
},
// 开发阶段插件
plugins:[
// HotModuleReplacementPlugin 用于实现热更新模块替换 ( Hot Module Replacement 简称HMR)
// 模块热更新允许在应用程序运行时替换模块
// 极大的提升开发效率,因为能让应用程序一直保持运行状态
new webpack.HotModuleReplacementPlugin({
multiStep: false,
})
]
});
module.exports = {
// webpack 配置
webpackConfig,
// devserver 配置,暴露给 dev.js 使用
DEV_SERVER_CONFIG
};
webpack.prod.js
js
const path = require('path');
const merge = require('webpack-merge');
const os = require('os');
const HappyPack = require('happypack'); // 处理多线程的
const MinCssExtractPlugin = require('mini-css-extract-plugin');
const CleanWebpcckPlugin = require('clean-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackInjectAttributesPlugin = require('html-webpack-inject-attributes-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
// 基类配置
const baseConfig = require('./webpack.base.js');
// 多想层 build 设置
const happypackCommonConfig = {
debug: false,
// 拿到 os.cpus().length 的数量,拿到多少就开多少核的线程
threadPool: HappyPack.ThreadPool({ size: os.cpus().length })
}
// 生产环境 webpack 配置
const webpackConfig = merge.smart(baseConfig, {
// 指定生产环境模式
mode: 'production',
// 生产环境的output配置
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.join(process.cwd(), './app/public/dist/prod'),//产生具体文件的路径
publicPath: '/dist/prod',
crossOriginLoading: 'anonymous',
},
module: {
rules: [{
test: /\.css$/,
use: [
MinCssExtractPlugin.loader,
'happypack/loader?id=css'
]
}, {
test: /\.js$/,
include: [
//只对业务代码进行 babel, 加快 webpack 打包速度
path.resolve(process.cwd(), './app/pages')
],
use: ['happypack/loader?id=js']
}]
},
// webpack 不会有大量 hints 信息,默认为 warning
performance: {
hints: false,
},
plugins: [
// 每次 build 前,清空 piblic/dist 目录
new CleanWebpcckPlugin(['pubilc/dist'],{
root: path.resolve(process.cwd(),'./app/'),
exclude:[],// 不排除任何文件
verbose: true,// 输出详细日志
dry: false, // 为false,不进入干运行模式,实际删除文件;为true,插件将进入 "dry-run" 模式,即模拟执行清理操作,但实际上并不会删除任何文件
}),
// 提取 css 的公共部分,有效利用缓存,(非公共部分使用 inline)
new MinCssExtractPlugin({
chunkFilename: 'css/[name]_[comtenthash:8].bundle.css'
}),
// 优化并压缩 css 资源
new CssMinimizerPlugin(),
// 多线程打包 js ,加快打包速度
new HappyPack({
...happypackCommonConfig,
id: 'js',
loaders: [`babel-loader?${JSON.stringify({
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime']
})}`]
}),
// 多线程打包 CSS , 加快打包速度
new HappyPack({
...happypackCommonConfig,
id: 'css',
loaders: [`babel-loader?${JSON.stringify({
path: 'css-loader',
options: {
importLoader: 1
}
})}`]
}),
// 浏览器在请求资源时不发送用户的身份凭证
new HtmlWebpackInjectAttributesPlugin({
crossorigin: 'anonymous'
})
],
optimization: {
// 使用 TerserWebpackPlugin 的并发和缓存,提升压缩简单性的性能
// 清楚 console.log
minimize: true,
minimizer: [
new TerserWebpackPlugin({
cache: true, //启用缓存来加快构建过程
parallel: true, // 利用多核 cpu 的优势来加快压缩速度
terserOptions: {
compress:{
drop_console: true,// 去掉 console.log 内容
}
}
})
]
}
});
module.exports = webpackConfig;
dev.js
js
// 本地开发启动 devaServer
const express = require('express');
const path = require('path');
const consoler = require('consoler'); //打印日志的,有颜色高亮的效果
const webpack = require('webpack');
const devMiddleware = require('webpack-dev-middleware');
const hotMiddleware = require('webpack-hot-middleware');
//从webpack.dev.js 获取 webpack 配置 和 devserver 配置
const { webpackConfig, DEV_SERVER_CONFIG } = require('./config/webpack.dev.js');
const app = express();
const compiler = webpack(webpackConfig);
// 指定静态文件目录
app.use(express.static(path.join(__dirname,'../public/dist')));
// 引用 devMiddleware 中间件 (监控文件改动)
app.use(devMiddleware(compiler,{
// 落地文件--不打包到内存里面
writeToDisk: (filePath) =>{ return filePath.endsWith('.tpl')},
// 资源路径 -- 与webpack.dev.js的output的publicPath路径相对应
publicPath: webpackConfig.output.publicPath,
// headers 配置 --可以访问跨域的内容
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Require-With, content-type, Authorization',
},
stats:{ colors: true}
}));
// 引用 hotMiddleware 中间件 (实现热更新通讯)
app.use(hotMiddleware(compiler,{
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log: () =>{}
}))
consoler.info('请等待webpack初次构建完成提示...')
// 启动服务 devServer ---- npm run build:dev
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port,() =>{
console.log(`app listening on port ${port}`);
})
prod.js
js
const webpack = require('webpack');
const webpackProdConfig = require('./config/webpack.prod.js');
console.log('\nbuilding... \n');
webpack(webpackProdConfig,(err,stats) =>{
if (err) {
console.log('err build... \n',err);
return;
}
//打包过程中出现的问题打印
process.stdout.write(`${stats.toString({
colors: true, // 在控制台输出色彩信息
modules: false, // 不显示每个模块的打包信息
children: false, // 不显示子编译任务信息
chunks: false, // 不显示每个代码快的信息
chunkModules: true, //显示代码快中模块的信息
})}\n`)
});