前端工程化之手搓webpack5 --【elpis全栈项目】
导读
基本流程:输入 -- 编译 -- 输出
被引擎读取 打包 输入--业务文件:pages 编译--引擎编译 输出--产物文件:dist
我们要做的就是:
- 配置引擎自动读取业务文件夹pages(比如一些
xxx.vue
文件); - 对业务代码进行编译、分包、压缩等操作;
- 得到一个dist文件夹(包含
html\css\js
等)
最后将dist部署到服务器就行了,这应该是任何一个工程化工具实现的最基本逻辑了吧。
所以,以webpack为例我们有必要认识一些基本配置属性:
以我的项目为例,我们来一步步配置出一个适合自己的webpack脚手架。以下是目录:
以下目录都在app文件夹下:
|-- pages 业务文件夹: 存放vue等文件
|-- public 产出文件夹: 打包后输出的dist文件会生成在这里
|-- webpack
|-- config webpack不同环境配置
|-- webpack.base.js
|-- webapck.dev.js
|-- webapck.prod.js
|-- dev.js 启动 开发 环境的的入口文件
|-- prod.js 启动 生产 环境的的入口文件
- 这是我的pages文件夹,用以实现一个项目多页面入口;(这个js就简单的认为他是个一般的SPA项目的
mian.js
)
- 最后
prod环境
要实现这样一个效果:将代码分割、分包、压缩、提取公共方法、树摇...等等。 dev环境
要实现这样一个效果:本地起一个服务,将dist放到本地的服务器上、实现HRM(热更新)
一、 基本配置: webpack.base.js
我们将公共配置,集中提取放置到这个base文件中,避免重复配置。
基本思路:
entry
:配置入口文件的路径
javascript
const pageEntries = {};
// 生成一个绝对路径,例如:/user/elpis/app/pages/**/entry.*.js
const entryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js');
// glob.sync是一个同步方法,会返回一个包含所有匹配文件路径的数组。
glob.sync(entryList).forEach(file => {
// file会输出:'/user/elpis/app/pages/page1/entry.page1.js'
const entryName = path.basename(file, '.js'); // 会提取文件名并去掉 .js, 例如:entry.page1
pageEntries[entryName] = file;
})
module.exports = {
// entry: {entry.page1: '/user/elpis/app/pages/page1/entry.page1.js', ...}
entry: pageEntries
}
module
:规定不同的文件,分别需要用什么loader去解析。用test
属性配置文件的匹配规则;用include
指定范围;用use
指定使用哪个loader
;options
则是对loader的配置。
javascript
module.exports ={
...
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.js$/,
include: [ path.resolve(process.cwd(), '/app/pages')], // 只对业务代码进行babel,加快打包速度
use: { loader: 'babel-loader' }
},
{
test: /\.(png|jpe?g|gif)(\?.+)?$/,
use: {
loader: 'url-loader',
options: {limit: 300, esMoule: false } // 小于300kb的图片会被转成base64编码、禁用esModule
}
}
]
}
...
}
resolve
: 配置一些解析时候的具体行为,是个优化项,看个人需要配置。例如
javascript
module.exports ={
...
resolve: {
// 定义别名,方便引入业务代码: import { xxx } from '$common/xxx';
alias: { $common: path.resolve(process.cwd(), './app/pages/common') }
}
...
}
plugins
: 使用插件。
javascript
module.exports ={
...
plugins: [
new VueLoaderPlugin() // 处理 .vue 文件,这个插件是必须的
...
]
...
}
optimization
: 输出优化。代码分割,模块分割,缓存,treeShaing,压缩等优化策略
javascript
module.exports ={
...
optimization: {
splitChunks:{...}, // 代码分割, 具体的看下文
runtimeChunk: true // 将 webpack 运行时生成的代码打包到 runtime.js
}
...
}
由此我们得到了一个完整的 base.js 的配置:
javascript
const glob = require('glob')
const path = require('path')
const webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const pageEntries = {};
const htmlWebpackPluginList = [];
// 获取 app/pages 目录下的所有页面入口文件(entry.xx.js)
const entryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js');
glob.sync(entryList).forEach(file => {
const entryName = path.basename(file, '.js');
// 构造 entry
pageEntries[entryName] = file;
// 构造最终渲染的页面文件
htmlWebpackPluginList.push(new HtmlWebpackPlugin({
// 产物最终模版
filename: path.resolve(process.cwd(), './app/public/dist/', `${entryName}.tpl`),
// 指定模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块
chunks: [entryName]
}))
})
/**
* webpack 基础配置
*/
module.exports = {
// 入口配置
entry: pageEntries,
// 模块解析配置
module: {
rules: [{
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
},
{
test: /\.js$/,
include: [
path.resolve(process.cwd(), '/app/pages') // 只对业务代码进行babel,加快打包速度
],
use: {
loader: 'babel-loader'
}
},
{
test: /\.(png|jpe?g|gif)(\?.+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 300, // 小于300kb的图片会被转成base64编码
esMoule: false // 禁用esModule
}
}
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader']
}, {
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/, // 例如:file.woff2?v=abc&opt=1
use: 'file-loader'
}
]
},
// 产物输出路径, 因为开发和生产环境输出不一致,所以在各自环境中进行配置
output: {},
// 配置模块解析的具体行为(定义 webpack 在打包时,如何找到并解析具体模块的路径)
resolve: {
// 尝试按顺序解析这些后缀名。如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀
// 能够使用户在引入模块时不带扩展:import File from '../path/to/file';
extensions: ['.js', '.vue', '.less', '.css'],
// 配置别名: import { xxx } from '$common/xxx';
alias: {
$page: path.resolve(process.cwd(), './app/pages'), // 定义别名,方便引入业务代码
$common: path.resolve(process.cwd(), './app/pages/common'),
$widgets: path.resolve(process.cwd(), './app/pages/widgets'),
$store: path.resolve(process.cwd(), './app/pages/store'),
}
},
// 配置 webpack 插件
plugins: [
// 处理 .vue 文件,这个插件是必须de
// 它的职能是将定义过的其他规则复制并应用到 .vue 文件中
// 例如,如果有一条匹配规则 /\.js$/ 的规则, 那么他会应用到 .vue 文件中的 script 板块中
new VueLoaderPlugin(),
// 把第三方库暴露到 window context 下
// 任何文件都可以直接使用 Vue,Webpack 会自动将其映射为 require('vue')。
// 例如 new Vue( { el: '#app', render: h => h(App) } );
new webpack.ProvidePlugin({ Vue: 'vue' }),
// 定义全局常量
new webpack.DefinePlugin({
__VUE_OPRIONS_API__: 'true', // 禁用选项式 API 支持
__VUE_PRO_DEVTOOLS: 'false', // 禁用 vue 调试工具
__VUE_PRO_HYDRATION_MISMATCH_DETAILS__: 'false' // 禁用生产环境构建下激活 (hydration) 不匹配的详细警告
}),
// 显示打包进度
new webpack.ProgressPlugin(),
// 每次 build 前清空 public/dist 目录
new CleanWebpackPlugin(['public/dist'], {
root: path.resolve(process.cwd(), './app/'),
exclude: [],
verbose: true,
dry: false
}),
// 构造最终渲染的页面模版
...htmlWebpackPluginList,
],
// 配置打包输出优化(代码分割,模块分割,缓存,treeShaing,压缩等优化策略)
optimization: {
/**
* 把 js 文件打包成3种类型
* 1. verdor: 第三方 lib 库, 基本不会改动, 除非依赖版本升级
* 2. common: 业务组件代码的公共部分抽取出来, 改动较少
* 3. ebnty.{page}: 不同页面 entry 里的业务组件代码的差异部分,会经常改动
* 目的: 把改动和引用频率不一样的 js 区分出来,已达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: 'all', // 对同步和异步模块都进行分割
maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
maxInitialRequests: 10, // 入口点的最大并行请求数
cacheGroups: {
vendor: { // 第三方库
test: /[\\/]node_modules[\\/]/, // 打包node_modules 目录下的模块
name: 'vendor', //模块名称
priority: 20, // 优先级,数字越大越优先
enforce: true, // 强制执行
reuseExistingChunk: true, // 复用已有的公共 chunk
},
common: { // 业务组件公共代码
name: 'common',
minChunks: 2, // 被两处引用即被归为公共模块
minSize: 1024 * 1, // 最小分割文件大小
priority: 10, // 优先级
reuseExistingChunk: true, // 复用已有的公共 chunk
}
},
},
// 将 webpack 运行时生成的代码打包到 runtime.js
runtimeChunk: true
},
}
二、 生产环境配置:webpack.prod.js + prod.js
生产环境的配置主要集中在打包优化上,比如:代码分割、压缩、分包、树摇等等。
基本思路:
- 基于base.js,合并配置
javascript
const baseConfig = require('./webpack.base.js');
const webpackConfig = merge.smart(baseConfig, {
mode:'',
output:''
...
})
mode
:指定模式,为生产环境。指定生产环境之后会默认开启一些配置,比如 tree sharking。
javascript
mode:'production'
output
: 配置产物输出路径。属性path
指定的是 Webpack 打包后文件输出的物理路径, 属性publicPath
指定的文件在服务器上的访问路径
javascript
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js', // 具体看 文档output/#template-strings
path: path.join(process.cwd(), './app/public/dist/prod'),
publicPath: '/dist/prod', // 输出目录的公共 URL
crossOriginLoading: 'anonymous' // 允许跨域加载
}
module
: 对每个模块要使用的loader等配置。使用thread-loader
实现多线程打包
javascript
module: {
rules: [
{
test: /\.js$/,
include: [path.resolve(process.cwd(), './app/pages')],
use: [{
loader: 'thread-loader', // 多线程编译loader
options: {
workers: os.cpus().length, // 使用 CPU 核心数
}
},
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'], // 用于语法转换和按需引入 polyfill (处理旧版本浏览器兼容,填补 API/新特性 缺失)
plugins: ['@babel/plugin-transform-runtime'] // 用于复用辅助代码和模块化 polyfill。
}
}
]
}]
}
plugins
: 插件配置。使用MiniCssExtractPlugin
插件,提取css公共部分等等。optimization
: 优化配置。使用TerserWebpackPlugin
提升压缩阶段的性能- 最后将webpack.prod.js的配置传入prod.js,prod.js通过
webpack()
方法启动打包
javascript
const webpackProdConfig = require('./config/webpack.prod.js');
webpack(webpackProdConfig, (err, stats) => {...做一些执行打包的回调处理}))
生产环境全量配置:wbepack.prod.js
javascript
const path = require('path');
const merge = require('webpack-merge');
const os = require('os');
const MiniCssExtractPlugin = require('mini-css-extract-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');
const webpackConfig = merge.smart(baseConfig, {
// 指定生产环境
mode: 'production',
// 生产环境的 out put 配置
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.join(process.cwd(), './app/public/dist/prod'),
publicPath: '/dist/prod', // 输出目录的公共 URL
crossOriginLoading: 'anonymous' // 允许跨域加载
},
module: {
rules: [{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "thread-loader"]
}, {
test: /\.js$/,
include: [path.resolve(process.cwd(), './app/pages')],
use: [{
loader: 'thread-loader', // 多线程编译loader
options: {
workers: os.cpus().length, // 使用 CPU 核心数
}
},
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'], // 用于语法转换和按需引入 polyfill (处理旧版本浏览器兼容,填补 API/新特性 缺失)
plugins: ['@babel/plugin-transform-runtime'] // 用于复用辅助代码和模块化 polyfill。
}
}
]
}]
},
// performance 用于控制性能提示信息, 默认为 warning; 文件体积过大, 入口过多, 资源加载方式等情况下会提示警告
performance: {
hints: false
},
plugins: [
// 提取 css 的公共部分, 有效利用缓存
new MiniCssExtractPlugin({
chunkFilename: 'css/[name]_[contenthash:8].bundle.css'
}),
// 优化并压缩 css
new CSSMinimizerPlugin(),
// 浏览器在请求资源时不发送用户的身份凭证
new HtmlWebpackInjectAttributesPlugin({
crossorgin: 'anonymous'
})
],
optimization: { // 优化配置
// 使用 TerserWebpackPlugin 的并发和缓存,提升压缩阶段的性能
minimize: true,
minimizer: [new TerserWebpackPlugin({
cache: true, // 启用缓存来加速构建过程
parallel: true, // 利用多核 CPU 并行压缩
terserOptions: {
compress: {
drop_console: true // 移除 console.log
}
}
})]
}
});
module.exports = webpackConfig;
生产环境启动配置:prod.js
javascript
const webpack = require('webpack');
const webpackProdConfig = require('./config/webpack.prod.js');
console.log('\nbuilding... \n');
// 如果你不向 webpack 传入可执行的回调函数, 它会返回一个 webpack Compiler 实例并在其中进行操作
// const compiler = webpack(webpackProdConfig);
// 区别在于compiler.run()更具灵活性、控制粒度等适合多次打包, 直接传入一个回调函数 (err, stats)=>{} 则只会执行一次打包, 更适用于生存环境的场景
webpack(webpackProdConfig, (err, stats) => {
// 配置文件错误
if (err) {
console.log('❗err: \n', err)
return
}
// stats.hasErrors()判断缺失的 module,语法错误等, 还有个 stats.hasWarnings() 方法,可以用来判断是否有警告信息
if (stats.hasErrors()) {
const info = stats.toJson()
console.error(info.errors);
}
// process.stdout.write 更高效,适合大量数据输出 (console.log 是基于它实现的)
process.stdout.write(`✅ ${stats.toString({
colors: true,// 在控制台输出色彩信息
modules: false, // 不显示每个模块的打包信息
children: false, // 不显示子编译任务的信息
chunks: false, // 不显示每个代码块的信息
chunkModules: false // 显示代码块中模块的信息
})}\n`)
});
三、 开发环境配置:webpack.dev.js + dev.js
开发环境则需要,配置 HMR 实现热更新
- 通过
merge
合并base和dev的配置
javascript
const webpackConfig = merge.smart(baseConfig,{
mode:'',
....
})
- 通过修改base.js的
entry
来配置HRM
javascript
const baseConfig = require('./webpack.base.js');
// 开发阶段的 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach(v => {
// 第三方包不作为 hmr 的入口
if (v !== 'vendor') {
baseConfig.entry[v] = [
baseConfig.entry[v], // 主入口文件
// hmr 更新入口,官方指定的 hmr 路径
`webpack-hot-middleware/client?path=http://${host}:${port}/${hmrPath}&timeout=${timeout}`,
]
}
})
- 指定
mode
等于 'development' devtool
: 等于 eval-cheap-module-source-map时。soure-map
配置 便于开发时调试output
: 配置产物输出路径。与生产环境不同的是,开发环境的产物需要放到本地服务器上。通过设置globalObject: 'this'
, Webpack 会根据运行环境自动选择正确的全局对象。
javascript
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.resolve(process.cwd(), './app/public/dist/dev/'),
publicPath: `http://${host}:${port}/public/dist/dev/`, // 输出目录的公共 URL
globalObject: 'this'
}
plugins
: 通过配置webpack.HotModuleReplacementPlugin
插件实现热模块替换
javascript
plugins: [ new webpack.HotModuleReplacementPlugin({ multiStep: false }) ]
- 自定义一个服务,将产物文件放到本地服务器上。主要是用到两个中间件: 用
devMiddleware
监控文件改动,用hotMiddleware
实现热更新,通知浏览器刷新
javascript
const { webpackConfig, DEV_SERVER_CONFIG } = require('./config/webpack.dev');
const app = express();
const compiler = webpack(webpackConfig)
app.use(devMiddleware(compiler, {}))
app.use(hotMiddleware(compiler, {}))
app.listen(DEV_SERVER_CONFIG.PORT, () => {})
开发环境打包配置:webpack.dev.js
javascript
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: 9200,
HMR_PATH: '__webpack_hmr',
TIMEOUT: 20000,
}
const { HOST: host, PORT: port, HMR_PATH: hmrPath, TIMEOUT: timeout } = DEV_SERVER_CONFIG;
// 开发阶段的 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach(v => {
console.log('--------v-------', v)
// 第三方包不作为 hmr 的入口 有时候可能会手动配置第三方库 entry: { vendor: ['vue', 'lodash']} 将其打包到一个单独的文件中
// 与 splitChunks 的区别: 自动从 node_modules 中提取第三方库。更灵活,适用于复杂的项目。
if (v !== 'vendor') {
baseConfig.entry[v] = [
baseConfig.entry[v], // 主入口文件
// hmr 更新入口,官方指定的 hmr 路径
`webpack-hot-middleware/client?path=http://${host}:${port}/${hmrPath}&timeout=${timeout}`,
]
}
})
const webpackConfig = merge.smart(baseConfig, {
// 指定开发环境
mode: 'development',
// soure-map 配置 便于开发时调试
devtool: 'eval-cheap-module-source-map',
// 开发环境的 out put 配置
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.resolve(process.cwd(), './app/public/dist/dev/'),
publicPath: `http://${host}:${port}/public/dist/dev/`, // 输出目录的公共 URL
globalObject: 'this' // 用于指定 Webpack 打包代码时引用的全局对象。配置成 'this' Webpack 会根据运行环境自动选择正确的全局对象。
},
// 开发阶段插件
plugins: [
// HotModuleReplacementPlugin 用于实现热模块替换 (Hot Module Replacement - HMR)
// 模块热替换允许在应用程序运行时替换模块
// 极大的提升开发效率, 因为能让应用程序一直保持运行状态
new webpack.HotModuleReplacementPlugin({ multiStep: false }),
]
});
module.exports = {
webpackConfig, // webpack 配置
DEV_SERVER_CONFIG // devServer 配置, 暴露给dev.js使用
};
开发环境启动配置:dev.js
javascript
// 本地开发启动devServer
const express = require('express');
const path = require('path');
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');
const app = express();
const compiler = webpack(webpackConfig)
// 指定静态文件目录
app.use(express.static(path.join(__dirname, '../public/dist')));
// 引用 devMiddleware 中间件 (监控文件改动)
app.use(devMiddleware(compiler, {
writeToDisk: (filPath) => filPath.endsWith('.tpl'), // 落地文件
publicPath: webpackConfig.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, contnet-type, Authorization'
},
stats: {
colors: true
}
}))
// 引用 hotMiddleware 中间件 (热更新)
app.use(hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log: () => { }
}))
console.info('请等待webpack初次构建完成...')
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
console.log("🚀 ~ app.listening on port:", port)
})
四、 配置npm启动打包
pagkage.json:
因为开发环境下,产物文件都放在本地服务器上,所以需要通过配置--max_old_space_size
分配好足够的内存。
javascript
"build:dev": "node --max_old_space_size=4096 ./app/webpack/dev.js",
"build:prod": "node ./app/webpack/prod.js"
更多参考:wbepack中文文档
全文特别鸣谢: 抖音"哲玄前端",《全栈实践课》