【webpack4系列】设计可维护的webpack4.x+vue构建配置(终极篇)
构建配置包设计
构建配置管理的可选方案:
- 通过多个配置文件管理不同环境的构建,webpack --config 参数进行控制
- 将构建配置设计成一个库,比如:xxx-webpack
- 抽成一个工具进行管理,比如:create-vue-app
- 将所有的配置放在一个文件,通过 --env 参数控制分支选择
通过多个配置文件管理不同环境的 webpack 配置
- 动态配置项:config.js
- 通用功能:utils.js
- 基础配置:webpack.base.js
- 开发环境:webpack.dev.js
- 生产环境:webpack.prod.js
- 预编译配置:webpack.dll.js
抽离成一个 npm 包统一管理(省略)
- 规范:Git commit日志、README、ESLint 规范、Semver 规范
- 质量:冒烟测试、单元测试、测试覆盖率和 CI
什么是semver 规范?
概念:语义化的版本控制(Semantic Versioning),简称语义化版本,英文缩写为 SemVer
优势:
- 避免出现循环依赖
- 依赖冲突减少
语义化版本(Semantic Versioning)规范格式
- 主版本号: 做了不兼容的 API 修改(进行不向下兼容的修改)
- 次版本号: 做了向下兼容的功能性增加(API 保持向下兼容的新增及修改)
- 修订号: 做了向下兼容的问题修正(修复问题但不影响 API)
通过 webpack-merge 组合配置
const merge = require("webpack-merge")
// 省略其他代码
module.exports = merge(baseConfig, devConfig);
功能模块设计
- 基础配置:webpack.base.js
- 资源解析
- 解析ES6
- 解析vue
- 解析css
- 解析less
- 解析scss
- 解析图片
- 解析字体
- 解析媒体
- 样式增强
- CSS前缀补齐
- CSS px转成rem等
- 目录清理
- 忽略打包内容
- 命令行信息显示优化
- 错误捕获和处理
- CSS提取成一个单独的文件
- 资源解析
- 开发配置:webpack.dev.js
- 代码热更新
- css热更新
- js热更新
- devServer配置
- sourcemap
- 样式压缩(可略)
- 资源拷贝(可略)
- 代码热更新
- 生产配置:webpack.prod.js
- 代码压缩
- 文件指纹
- Tree Shaking(webpack4自带)
- Scope Hositing(webpack4自带)
- 速度优化(基础包CDN等)
- 体积优化(代码分割)
- 资源拷贝(可略)
- 构建报告(可略)
- 构建速度(可略)
- 预编译配置:webpack.dll.js
- 基础库:vue、element-ui等
- 其它库:axios、vue-router等
- 通用功能:utils.js
- CSS加载器
- 资源路径
- 环境变量
- eslint检测配置
- ...
- 动态配置项:config.js
- dev: 开发动态配置项
- build:生产动态配置项
目录结构设计
- app-build 放置配置文件
- app-dll 放预编译后的文件
- src(或者lib)放置源代码
- test 放置测试代码(可省略)
结构如下:
+ |- /app-build
+ |- config.js
+ |- utils.js
+ |- webpack.dev.js
+ |- webpack.prod.js
+ |- webpack.base.js
+ |- webpack.dll.js
+ |- /app-dll
+ |- dll.library.min.js
+ |- dll.vendors.min.js
+ |- manifest.library.json
+ |- manifest.vendors.json
+ |- /test
+ |- /src
+ |- .env.development
+ |- .env.production
+ |- .eslintignore
+ |- .eslinrc.js
+ |- .prettierrc
+ |- package.json
+ |- README.md
构建配置插件
安装webpack、webpack-cli
npm i webpack@4 webpack-cli@4.10.0 -D
关联HTML插件html-webpack-plugin
webpack4.x对应的html-webpack-plugin@4
npm install html-webpack-plugin@4 -D
webpack示例配置:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
]
}
解析ES6
webpack4.x安装@babel/core,@babel/preset-env,babel-loader@8
npm i babel-loader@8 @babel/core @babel/preset-env core-js@3 -D
在根路径下新建一个.babelrc文件,增加ES6的babel preset配置,代码如下:
{
"preset": ["@babel/preset-env"]
}
webpack示例配置:
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
解析vue、JSX
安装插件:
npm i @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props vue-loader@15 vue-style-loader@3 vue-template-compiler -D
npm i vue@2 -S
在.babelrc文件中添加JSX相关配置:
{
"presets": [["@babel/preset-env"], "@vue/babel-preset-jsx"]
}
解析CSS、Less和Sass
解析CSS
解析css,需要安装style-loader和css-loader。
其中webpack4.x安装style-loader1.x、css-loader4.x:
npm i css-loader@4 style-loader@1 -D
rules配置:
{
test: /.css$/,
use: ['style-loader', 'css-loader']
}
解析Less
解析less,需要安装less、less-loader。
其中webpack4.x建议安装less-loader@6(less-loader@7.0.1也支持webpack4.x)
npm i less less-loader@6 -D
版本参考:https://github.com/webpack-contrib/less-loader/blob/v6.2.0/package.json
rules配置如下:
{
test: /.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
解析sass
安装sass、sass-loader、sass-resources-loader(剔除掉node-sass,深度依赖node版本):
npm i sass@1.32.13 sass-loader@7.3.1 sass-resources-loader@2.2.4 -D
注意:node-sass可以解析/deep/、::v-deep,sass只能解析::v-deep,如果剔除掉node-sass,需要把相关语法升级。像/deep/、::v-deep、:deep()这些是Vue.js框架中用于穿透样式作用域的特定选择器。
以下是对/deep/
、::v-deep
和:deep()
的对比说明:
选择器 | 说明 | 应用示例 |
---|---|---|
/deep/ | 是一个用于穿透组件样式作用域的旧版选择器。在某些Vue版本中被移除,不建议使用。 | 在Vue 2.x中,用于穿透样式作用域,如:.parent /deep/ .child { ... } |
::v-deep | 是Vue中的一个内置伪选择器,用于访问子组件的样式。它只适用于scoped样式中,并将样式深入到子组件作用域中。 | 在Vue组件的<style scoped> 标签中使用,如:.parent ::v-deep .child { ... } |
:deep() | 是/deep/的替代品,也是一个伪类选择器。适用于全局样式和嵌套组件中的样式,用于穿透样式作用域。 | 可用于Vue组件的<style> 标签中,无论是否带有scoped 属性,如:.parent :deep(.child) { ... } |
提取CSS
如果需要单独把 CSS 文件分离出来,我们需要使用 mini-css-extract-plugin 插件。
注:v4 版本之后才开始使用 mini-css-extract-plugin,之前的版本是使用 extract-text-webpack-plugin。
安装mini-css-extract-plugin插件:
npm i mini-css-extract-plugin -D
解析图片和字体
资源解析:解析图片
解析图片,可以安装file-loader,其中file-loader最新版本为6.2.0,支持webpack4.x。
npm i file-loader -D
版本参考:https://github.com/webpack-contrib/file-loader/blob/v6.2.0/package.json
rules配置如下:
{
test: /.(png|jpe?g|gif)$/,
use: 'file-loader'
}
资源解析:解析字体
rules配置如下:
{
test: /.(woff|woff2|eot|otf|ttf)$/,
use: 'file-loader'
},
css参考样式:
@font-face {
font-family: 'SourceHeavy';
src: url('./images/SourceHeavy.otf') format('truetype');
}
.search-text {
font-size: 20px;
color: #f00;
font-family: 'SourceHeavy';
}
资源解析:使用url-loader
url-loader 也可以处理图⽚和字体,可以设置较⼩资源⾃动 base64,其中url-loader内部实现也是使用的file-loader。
目前url-loader最新版本为4.1.1,支持webpack4.x.
npm i url-loader -D
版本参考:https://github.com/webpack-contrib/url-loader/blob/master/package.json
rules配置(把之前关于图片的file-loader配置替换):
{
test: /.(png|jpe?g|gif)$/,
use: [{ loader: 'url-loader', options: { limit: 10240 } }],
}
热更新:webpack-dev-server
- webpack-dev-server不刷新浏览器
- webpack-dev-server不输出⽂件,⽽是放在内存中
- 使⽤ HotModuleReplacementPlugin插件
npm i webpack-dev-server@3 -D
package.json示例配置:
"scripts": {
"dev": "webpack-dev-server --open"
}
其中
open
是构建完成之后,自动开启浏览器。
文件指纹策略:chunkhash、contenthash和hash
注:文件指纹只能用于生产环境。
文件指纹如何生成
- Hash:和整个项⽬的构建相关,只要项⽬⽂件有修改,整个项⽬构建的hash值就会更改
- Chunkhash:和webpack 打包的chunk 有关,不同的entry 会⽣成不同的chunkhash值
- Contenthash:根据⽂件内容来定义hash ,⽂件内容不变,则contenthash不变
文件指纹设置
- JS文件:设置output的filename,使⽤[chunkhash]。
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
}
- CSS文件:设置MiniCssExtractPlugin的filename,使⽤[contenthash]
new MiniCssExtractPlugin({
filename: "[name]_[contenthash:8].css"
}),
HTML 、CSS和JavaScript代码压缩
JS压缩
webpack4及以后使用内置optimization,配合自定义压缩插件terser-webpack-plugin
使用
npm i terser-webpack-plugin@4 -D
配置示例:
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
// 过滤掉以".min.js"结尾的文件.
exclude: /\.min\.js$/i,
// Enable multi-process parallel running and set number of concurrent runs.
parallel: true,
// Enable file caching. Default path to cache directory: node_modules/.cache/terser-webpack-plugin.
cache: true,
terserOptions: {
// 开启变量名混淆
mangle: true,
compress: {
unused: true,
// 移除所有debugger
drop_debugger: true,
// 移除所有console
drop_console: true,
pure_funcs: [
// 移除指定的指令,如console, alert等
"console.log",
"console.error",
"console.dir"
]
},
format: {
// 删除注释
comments: true
}
},
// 是否将注释剥离到单独的文件中
extractComments: false
})
]
}
CSS压缩
需要安装optimize-css-assets-webpack-plugin
,同时使⽤cssnano
。
说明:optimize-css-assets-webpack-plugin插件目前官网最新版本5.0.8,使用的webpack为^4.44.1。
npm i optimize-css-assets-webpack-plugin@5 cssnano@4 -D
插件配置地址:https://github.com/NMFR/optimize-css-assets-webpack-plugin/blob/master/package.json
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
// 其他省略
mode: 'production',
plugins: [
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
}),
],
}
另外官网推荐了另外一个CSS样式插件:css-minimizer-webpack-plugin
。如果在开发环境提取的CSS需要压缩,建议使用css-minimizer-webpack-plugin
插件,测试压缩速度比optimize-css-assets-webpack-plugin
快。
安装:
npm i css-minimizer-webpack-plugin@1.3.0 -D
示例配置:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin({
test: /\.css$/i
})
]
}
}
HTML压缩
安装html-webpack-plugin,并设置压缩参数。
其中webpack4.x对应的html-webpack-plugin@4。
npm i html-webpack-plugin@4 -D
webpack示例配置:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html'),
filename: 'index.html',
chunks: ['index'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false,
},
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/search.html'),
filename: 'search.html',
chunks: ['search'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false,
},
})
]
}
自动清理构建目录产物
webpack4.x使用clean-webpack-plugin@3版本:
npm i clean-webpack-plugin@3 -D
webpack配置:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins: [
new CleanWebpackPlugin(),
]
PostCSS插件autoprefixer自动补齐CSS3前缀
需要安装postcss-loader、postcss、autoprefixer插件。
其中webpack4.x需要安装postcss-loader@4。
npm i postcss-loader@4 postcss@8 autoprefixer -D
示例配置如下:
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'autoprefixer',
{
overrideBrowserslist: ['last 2 version', '>1%', 'ios 7']
}
]
]
}
}
}
]
}
]
}
}
静态资源内联
资源内联的意义:
- 代码层⾯:
- ⻚⾯框架的初始化脚本
- 上报相关打点
- css 内联避免⻚⾯闪动
- 请求层⾯:减少 HTTP ⽹络请求数
- ⼩图⽚或者字体内联 (url-loader)
安装raw-loader@0.5.1版本
npm i raw-loader@0.5.1 -D
- raw-loader 内联 html
<%= require('raw-loader!./meta.html') %>
- raw-loader 内联 JS
<%= require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js') %>
示例,例如我们抽离meta通用的代码为一个meta.html,以及flexible.js插件都内联带html页面中。
meta.html示例代码:
<meta charset="UTF-8">
<meta name="viewport" content="viewport-fit=cover,width=device-width,initial-scale=1,user-scalable=no">
<meta name="format-detection" content="telephone=no">
<meta name="keywords" content="keywords content">
<meta name="name" itemprop="name" content="name content">
<meta name="apple-mobile-web-app-capable" content="no">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
使用sourcemap
作⽤:通过source map定位到源代码
sourcemap参考文章:http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html
source map 关键字:
- eval: 使⽤eval包裹模块代码
- source map: 产⽣.map⽂件
- cheap: 不包含列信息
- inline: 将.map作为DataURI嵌⼊,不单独⽣成.map⽂件
- module:包含loader的sourcemap
source map 类型:
一般开发环境配置:
module.exports = {
// 其他代码省略
devtool: "source-map"
};
生产环境配置:
module.exports = {
// 其他代码省略
devtool: "none"
};
Tree Shaking(摇树优化)的使用和原理分析
基础介绍
一个模块可能有多个⽅法,只要其中的某个⽅法使⽤到了,则整个⽂件都会被打到
bundle ⾥⾯去,tree shaking 就是只把⽤到的⽅法打⼊ bundle ,没⽤到的⽅法会在
uglify 阶段被擦除掉。
uglify阶段:将 JavaScript代码进行压缩、混淆,并去除一些不必要的代码,从而减小文件体积。
webpack4及以上默认内置了,当mode为production情况下默认开启。进行tree shaking条件是必须是 ES6 的语法,CJS 的⽅式不⽀持。
DCE (Dead code elimination)
DCE 解释就是死代码消除的意思。
- 代码不会被执⾏,不可到达
- 代码执⾏的结果不会被⽤到
- 代码只会影响死变量(只写不读)
示例:
if (false) {
console.log('这段代码永远不会执行’);
}
如上所示代码,在uglify 阶段就会删除⽆⽤代码。
Tree-shaking 原理
利⽤ ES6 模块的特点:
- 只能作为模块顶层的语句出现
- import 的模块名只能是字符串常量
- import binding 是 immutable的(import引用的模块是不能修改的)
注:使用mode为production与none 来验证tree-shaking。
Scope Hoisting使用和原理分析
背景:构建后的代码存在⼤量闭包代码
如图所示:
这样会导致什么问题?
- ⼤量作⽤域包裹代码,导致体积增⼤(模块越多越明显)
- 运⾏代码时创建的函数作⽤域变多,内存开销变⼤
模块转换分析
示例:我们编写了一个模块,代码如下
import { helloworld } from "./helloworld";
import "../../common";
document.write(helloworld());
我们把webpack4中的mode设置为none,看下编译结果,webpack会把编写的模块转换成模块初始化函数,代码如下:
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _helloworld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
document.write(Object(_helloworld__WEBPACK_IMPORTED_MODULE_0__["helloworld"])());
/***/ })
结果说明:
- 被 webpack 转换后的模块会带上⼀层包裹
- import 会被转换成 __webpack_require
当然上面两个import导入的模块编译为如下代码:
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helloworld", function() { return helloworld; });
function helloworld() {
return 'Hello webpack';
}
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "common", function() { return common; });
function common() {
return "common module";
}
/***/ })
进⼀步分析 webpack 的模块机制
(function (modules) {
// webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
});
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
return __webpack_require__(0);
})([
/* 0 */
function (module, __webpack_exports__, __webpack_require__) {
// 省略代码
},
/* 1 */
function (module, __webpack_exports__, __webpack_require__) {
// 省略代码
},
/* 2 */
function (module, __webpack_exports__, __webpack_require__) {
// 省略代码
}
/******/
]);
上述代码分析:
- 打包出来的是⼀个 IIFE (匿名闭包)
- modules 是⼀个数组,每⼀项是⼀个模块初始化函数
- __webpack_require ⽤来加载模块,返回 module.exports
- 通过 webpack_require(0) 启动程序
scope hoisting 原理
原理:将所有模块的代码按照引⽤顺序放在⼀个函数作⽤域⾥,然后适当的重命名⼀
些变量以防⽌变量名冲突。
优点:通过scope hoisting
可以减少函数声明代码和内存开销。
优化前代码:
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helloworld", function() { return helloworld; });
function helloworld() {
return 'Hello webpack';
}
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "common", function() { return common; });
function common() {
return "common module";
}
/***/ })
优化后代码:
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/index/helloworld.js
function helloworld() {
return 'Hello webpack';
}
// CONCATENATED MODULE: ./common/index.js
function common() {
return "common module";
}
// CONCATENATED MODULE: ./src/index/index.js
document.write(helloworld());
})
scope hoisting 使⽤
webpack mode 为 production 默认开启,必须是 ES6
语法,CJS
不⽀持。
由于mode为production来验证的话,默认会被压缩,我们可以设置为none,然后添加ModuleConcatenationPlugin来验证,示例代码:
const webpack = require("webpack");
module.exports = {
// 其他代码省略
mode: "none",
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
注:webpack4及以上mode为production的时候,默认内置了ModuleConcatenationPlugin
代码分割和动态import
代码分割的意义
对于⼤的 Web 应⽤来讲,将所有的代码都放在⼀个⽂件中显然是不够有效的,特别是当你的
某些代码块是在某些特殊的时候才会被使⽤到。webpack 有⼀个功能就是将你的代码库分割成
chunks(语块),当代码运⾏到需要它们的时候再进⾏加载。
适⽤的场景:
- 抽离相同代码到⼀个共享块
- 脚本懒加载,使得初始下载的代码更⼩
懒加载 JS 脚本的⽅式
- CommonJS:require.ensure
- ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换)
如何使⽤动态 import?
安装 babel 插件
npm i @babel/plugin-syntax-dynamic-import -D
ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换),在babelrc中添加:
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
代码分割的效果如图所示:
上面编译的圈红的,如果是动态加载的,那会生成一个以[number]_[chunkhash].js生成的文件名。
在webpack中使用ESLint
行内优秀的eslint规范
- Airbnb:
eslint-config-airbnb
、eslint-config-airbnb-base
- alloyteam团队 eslint-config-alloy:https://github.com/AlloyTeam/eslint-config-alloy
eslint-config-airbnb:默认导出包含大多数ESLint规则,包括ECMAScript 6+和React。它需要eslint, eslint-plugin-import, eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-jsx-a11y。请注意,它不会启用我们的React Hooks规则。
当然如果不需要React,那么可以参考使用eslint-config-airbnb-base。
以使用eslint-config-airbnb-base为例:
npm i eslint@7 babel-eslint@10 eslint-webpack-plugin@2 eslint-config-airbnb-base eslint-plugin-import -D
其中使用eslint-webpack-plugin替换eslint-loader.
eslint-webpack-plugin 3.0 which works only with webpack 5. For the webpack 4, see the 2.x branch.
eslint-webpack-plugin详细参考地址:https://github.com/webpack-contrib/eslint-webpack-plugin
示例代码:
const ESLintPlugin = require("eslint-webpack-plugin");
module.exports = {
mode: "production",
plugins: [
new ESLintPlugin({
fix: true, // 启用ESLint自动修复功能
extensions: ["js", "jsx"],
context: path.join(__dirname, "src"), // 文件根目录
exclude: ["/node_modules/"], // 指定要排除的文件/目录
cache: true // 缓存
})
]
};
再安装如下插件:
npm i eslint-import-resolver-webpack eslint-plugin-vue@7.18.0 vue-eslint-parser@7.11.0 -D
- eslint-import-resolver-webpack:获取webpack配置的一些参数,共享给配置规则,让其正确识别import路径。
- eslint-plugin-vue:帮助我们检测.vue文件中
<template>
和<script>
中的js代码 - vue-eslint-parser:eslint-plugin-vue 插件依赖 vue-eslint-parser解析器
整体示例代码:
const webpackBaseConfig = require('./app-build/webpack.base.js');
module.exports = {
root: true,
parser: 'vue-eslint-parser',
parserOptions: {
parser: 'babel-eslint'
},
env: {
node: true,
browser: true,
es6: true
},
plugins: ['vue'],
extends: [
'plugin:vue/essential',
'airbnb-base'
],
settings: {
'import/resolver': {
webpack: {
config : {
resolve: webpackBaseConfig.resolve
}
}
}
},
rules: {
}
};
优化构建时命令行的显示日志
webpack构建统计信息 stats
如何优化命令⾏的构建⽇志
1、使⽤ friendly-errors-webpack-plugin
- success: 构建成功的⽇志提示
- warning: 构建警告的⽇志提示
- error: 构建报错的⽇志提示
2、stats 设置成 errors-only
安装friendly-errors-webpack-plugin:
npm i friendly-errors-webpack-plugin -D
示例配置:
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
module.exports = {
plugins: [
new FriendlyErrorsWebpackPlugin()
],
stats: "errors-only"
};
构建异常和中断处理
如何判断构建是否成功?
- 在 CI/CD 的 pipline 或者发布系统需要知道当前构建状态
- 每次构建完成后输⼊ echo $? 获取错误码
webpack4 之前的版本构建失败不会抛出错误码 (error code)
Node.js 中的 process.exit 规范
- 0 表示成功完成,回调函数中,err 为 null
- ⾮ 0 表示执⾏失败,回调函数中,err 不为 null,err.code 就是传给 exit 的数字
如何主动捕获并处理构建错误?
- compiler 在每次构建结束后会触发 done 这个 hook
- process.exit 主动处理构建报错
在配置中可以添加如下代码,进行中断处理,例如错误上报等。
module.exports = {
plugins: [
function errorPlugin() {
this.hooks.done.tap("done", (stats) => {
if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf("--watch") == -1) {
console.log("build error");
process.exit(1); // 1表示错误码并退出
}
});
}
]
};
速度分析:使用 speed-measure-webpack-plugin
使用speed-measure-webpack-plugin插件。
官网地址:https://github.com/stephencookdev/speed-measure-webpack-plugin#readme
安装:
npm i speed-measure-webpack-plugin -D
使用:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// 其他省略
plugins: [
new MyPlugin(), new MyOtherPlugin()
]
});
速度分析插件作用:
- 分析整个打包总耗时
- 每个插件和loader的耗时情况
体积分析:使用webpack-bundle-analyzer
安装:
npm i webpack-bundle-analyzer -D
示例:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
可以分析哪些问题?
- 依赖的第三方模块文件大小
- 业务里面的组件代码大小
多进程/多实例构建
资源并行解析可选方案
- parallel-webpack
- HappyPack
- thread-loader
使用 HappyPack 解析资源
原理:每次 webapck 解析一个模块,HappyPack 会将它及它的依赖分配给 worker 线程中。
安装:
npm i happypack -D
使用示例:
const HappyPack = require('happypack');
exports.module = {
rules: [
{
test: /.js$/,
// 1) replace your original list of loaders with "happypack/loader":
// loaders: [ 'babel-loader?presets[]=es2015' ],
use: 'happypack/loader',
include: [ /* ... */ ],
exclude: [ /* ... */ ]
}
]
};
exports.plugins = [
// 2) create the plugin:
new HappyPack({
// 3) re-add the loaders you replaced above in #1:
loaders: [ 'babel-loader?presets[]=es2015' ]
})
];
使用 thread-loader 解析资源
由于webpack4.x目前只能安装thread-loader@3.0.0版本,3.0.0以后的版本需要webpack5.x。
原理:每次 webpack 解析一个模块,thread-loader 会将它及它的依赖分配给 worker 线程中
npm i thread-loader@3.0.0 -D
配置:
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: "thread-loader",
options: {
workers: 3
}
},
"babel-loader"
]
}
]
}
多进程并行压缩代码:terser-webpack-plugin 开启 parallel 参数
webpack4.x及以上建议使用terser-webpack-plugin插件
注:Using Webpack v4, you have to install terser-webpack-plugin v4.
安装:
npm i terser-webpack-plugin@4 -D
配置示例:
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: true
})
]
}
};
进一步分包:预编译资源模块
方法:使用DLLPlugin进行分包,DllReferencePlugin对 manifest.json 引用
- DllPlugin:负责抽离第三方库,形成第三方动态库dll。
- DllReferencePlugin:负责引用第三方库。
使用 DLLPlugin 进行分包
新建一个webpack.dll.js:
const path = require("path");
const webpack = require("webpack");
module.exports = {
entry: {
library: ["vue/dist/vue.esm.js", "element-ui"]
},
output: {
filename: "[name].dll.js",
path: path.join(__dirname, "build/library"),
library: "[name]_[hash:8]", // 保持与webpack.DllPlugin中name一致
libraryTarget: 'window'
},
resolve: {
alias: {
vue$: "vue/dist/vue.esm.js"
}
},
plugins: [
new webpack.DllPlugin({
name: "[name]_[hash:8]", // 保持与output.library中名称一致
path: path.join(__dirname, "build/library/[name].json")
})
]
};
在package.json中添加命令:
"scripts": {
"dll": "webpack --config webpack.dll.js"
}
最后执行npm run dll,结果在工程根目录下有如下文件:
- build
- library.dll.js
- library.json
使用 DllReferencePlugin 引用 manifest.json
在webpack.prod.js中插件中配置如下:
plugins: [
new webpack.DllReferencePlugin({
manifest: require("./build/library/library.json")
})
]
当执行npm run build
后其实index.html页面中没有引入library.dll.js
文件,我们可以通过安装add-asset-html-webpack-plugin
插件,webpack4.x
版本使用add-asset-html-webpack-plugin@3
npm i add-asset-html-webpack-plugin@3 -D
在webpack.prod.js中插件中配置如下:
plugins: [
new webpack.DllReferencePlugin({
manifest: require("./build/library/library.json")
}),
new AddAssetHtmlPlugin({
filepath: path.resolve("./build/library", "library.dll.js")
})
]
作用就是把build/library/library.dll.js拷贝到编译后的dist文件夹下,并且通过script标签引入到index.html中。
最终页面生成的效果:
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<head>
<title>Document</title>
<link href="search_42937580.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script src="library.dll.js"></script>
<script src="search_c1f12d25.js"></script>
</body>
</html>
add-asset-html-webpack-plugin参考地址:https://www.npmjs.com/package/add-asset-html-webpack-plugin/v/3.2.2?activeTab=versions
充分利用缓存提升二次构建速度
目的:提升二次构建速度。
缓存思路:
- babel-loader 开启缓存
- terser-webpack-plugin 开启缓存
- 使用 cache-loader 或者 hard-source-webpack-plugin
开启了对应方式的缓存,会在node_modules目录下的cache文件夹看到缓存的内容,如下结构:
- node_modules
- .cache
- babel-loader
- hard-source
- terser-webpack-plugin
- .cache
1、babel-loader 开启缓存
rules: [
{
test: /.js$/,
use: ["babel-loader?cacheDirectory=true"
]
}
]
如果是使用的HappyPack,配置如下:
new HappyPack({
loaders: ["babel-loader?cacheDirectory=true"]
})
2、terser-webpack-plugin 开启缓存
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
cache: true
})
]
}
3、hard-source-webpack-plugin开启缓存
安装:
npm i hard-source-webpack-plugin -D
配置:
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
plugins: [
new HardSourceWebpackPlugin()
]
}
在webpack4.x中有时会报错。
缩小构建目标与减少文件搜索范围
缩小构建目标
目的:尽可能的少构建模块
比如 babel-loader 不解析 node_modules
rules: [
{
test: /.js$/,
include: [path.resolve(__dirname, "src")],
use: [
"babel-loader"
]
}
当然也可以使用exclude,来缩小构建范围。
减少文件搜索范围
- 优化 resolve.modules 配置(减少模块搜索层级)
- 优化 resolve.mainFields 配置
- 优化 resolve.extensions 配置
- 合理使用 alias
示例代码:
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
react: path.resolve(__dirname, "./node_modules/react/umd/react.production.min.js"),
"react-dom": path.resolve(__dirname, "./node_modules/react-dom/umd/react-dom.production.min.js")
extensions: [".js"],
mainFields: ["main"]
},
擦除无用的CSS
无用的 CSS 如何删除掉?
- PurifyCSS: 遍历代码,识别已经用到的 CSS class
- uncss: HTML 需要通过 jsdom加载,所有的样式通过PostCSS解析,通过document.querySelector 来识别在 html 文件里面不存在的选择器
在 webpack 中如何使用 PurifyCSS?
PurifyCSS官网已经不再维护了,使用 purgecss-webpack-plugin
这个插件和 mini-css-extract-plugin
配合使用。
安装purgecss-webpack-plugin
插件:
npm i purgecss-webpack-plugin@4 glob@7 -D
配置:
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const PATHS = {
src: path.join(__dirname, 'src')
}
module.exports = {
// 其他省略
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new PurgeCSSPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
]
}
配置其它插件
- webpack-merge:合并webpack配置项
npm i webpack-merge -D
- copy-webpack-plugin:拷贝资源
npm i @copy-webpack-plugin6 -D
- git-revision-webpack-plugin:获取git信息
npm i git-revision-webpack-plugin -D
- progress-bar-webpack-plugin:编译进度
npm i progress-bar-webpack-plugin -D
- cross-env:设置node环境变量
npm i cross-env -D
- portfinder:查找机器端口
npm i portfinder -D
示例:
module.exports = new Promise((resolve, reject) => {
portfinder.setBasePort(process.env.PORT || config.dev.port);
portfinder.getPort(function (err, port) {
//
// `port` is guaranteed to be a free port
// in this scope.
//
if (err) {
reject(err);
} else {
process.env.PORT = port;
devConfig.devServer.port = port;
resolve(merge(baseConfig, devConfig));
}
});
});
- node-notifier:用 Node.js 发送跨平台本机通知
npm i node-notifier -D
示例:
const notifier = require('node-notifier');
// String
notifier.notify('Message');
// Object
notifier.notify({
title: 'My notification',
message: 'Hello, there!'
});
- prettier:代码格式化插件
npm i prettier -D
实现源码
/app-build/
config.js
const path = require("path");
module.exports = {
// 开发基础配置
dev: {
// 静态资源存放的文件夹
assetsSubDirectory: "static",
// 静态资源引用路径
assetsPublicPath: "/",
proxy: {},
// can be overwritten by process.env.HOST
host: "localhost",
// can be overwritten by process.env.PORT, if port is in use, a free one will be determined
port: 8081,
// css source map
cssSourceMap: false,
// source map类型
devtool: "cheap-source-map",
// 是否自动打开浏览器
open: true,
// 是否添加git信息
gitRevision: true,
// 是否启用eslint
useEslint: true,
// 开启eslint后,检测任何出错是否导致编译失败,true=是,false=否
esLintFailOnError: false,
},
// 生产基础配置
build: {
// 打包后的文件存放的地方
assetsRoot: path.resolve(__dirname, "../dist"),
// 静态资源存放的文件夹
assetsSubDirectory: "static",
// 静态资源引用路径
assetsPublicPath: "./",
// 是否开启source map
productionSourceMap: false,
// source map类型
devtool: "cheap-source-map",
// 是否开启打包后的分析报告
bundleAnalyzerReport: false,
// 测量webpack构建速度
isSpeedMeasure: false,
// 是否添加git信息
gitRevision: true,
// 清除多余不用的css样式
purgeCSS: true,
// 是否启用eslint
useEslint: true,
// 开启eslint后,检测任何出错是否导致编译失败,true=是,false=否
esLintFailOnError: true
}
};
app-build/utils.js
const path = require("path");
const { merge } = require("webpack-merge");
const fs = require("fs");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const GitRevisionPlugin = require("git-revision-webpack-plugin");
const ESLintPlugin = require("eslint-webpack-plugin");
const config = require("./config");
// 生成css加载器
exports.cssLoaders = function (options) {
// css loader
const cssLoader = {
loader: "css-loader",
options: {
sourceMap: options.sourceMap,
modules: {
compileType: 'icss'
}
}
};
// postcss loader
const postcssLoader = {
loader: "postcss-loader",
options: {
sourceMap: options.sourceMap,
postcssOptions: {
plugins: [
[
"autoprefixer",
{
overrideBrowserslist: ["last 2 version", ">1%", "ios 7"]
}
]
]
}
}
};
/**
*
* @param { Sting } loader loader名称
* @param { Object } loaderOptions loader配置项
* @param { Object } injectOptions 匹配 css-loader或者postcss-loader等基础loader,把loaderOptions配置项注入其中
* @returns 返回loader数组
*/
function generateLoaders(loader, loaderOptions) {
const loaders = options.usePostCSS
? [cssLoader, postcssLoader]
: [cssLoader];
if (loader) {
loaders.push({
loader: `${loader}-loader`,
options: {
...loaderOptions,
sourceMap: options.sourceMap
}
});
}
// 如果需要提前css文件,就使用MiniCssExtractPlugin.loader
if (options.extract) {
return [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: "../../"
}
},
...loaders
];
}
return ["style-loader", ...loaders];
}
return [
{
test: /\.(sa|sc|c)ss$/,
use: generateLoaders("sass")
},
{
test: /.less$/,
use: generateLoaders("less")
}
];
};
// 设置资源路径
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === "production"
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory;
// 在 Windows 平台上,路径分隔符是 \(也可以使用 /),而在其他平台上是 /
// path.join 即会按照当前操作系统进行给定路径分隔符,而 path.posix.join 则始终是 /
return path.posix.join(assetsSubDirectory, _path);
};
// 获取绝对路径
exports.resolve = function (dir) {
return path.join(__dirname, "..", dir);
};
// 获取config文件中的环境属性名称
exports.getEnvName = function () {
const env = process.env.NODE_ENV;
let configEnvName = "";
if (env === "development") {
configEnvName = "dev";
}
if (env === "production") {
configEnvName = "build";
}
return configEnvName;
};
// 获取环境变量
exports.getEnvironment = function () {
const envName = exports.getEnvName();
// 环境变量中是否需要添加git信息
let gitEnv = {};
if (config[envName].gitRevision) {
const gitRevision = new GitRevisionPlugin();
gitEnv = {
GITVERSION: JSON.stringify(gitRevision.version()),
GITBRANCH: JSON.stringify(gitRevision.branch())
};
}
// 本地环境文件解析
let localEnv = {};
const callbackData = fs.readFileSync(
path.join(__dirname, "..", `.env.${process.env.NODE_ENV}`)
);
const result = callbackData.toString();
if (result) {
localEnv = result.replace(/[\r\n]/g, ";").split(";").reduce((pre, cur) => {
const [key, value] = cur.split("=");
if (key) {
pre[key] = JSON.stringify(value);
process.env[key] = value;
}
return pre;
}, {});
}
return merge({}, gitEnv, localEnv);
};
/** 创建eslint检测插件
* @param { Boolean } failOnError 如果出现任何错误,将导致模块构建失败 true=是 false=否
* @returns plugin Object
*/
exports.createEsLintPlugin = function(failOnError) {
// eslint-webpack-plugin详细参考: https://www.npmjs.com/package/eslint-webpack-plugin
return new ESLintPlugin({
// 自动修复
fix: true,
// 指定应检查的扩展
extensions: ["js", "jsx","vue"],
// 指定检测的目录
context: exports.resolve("src"),
// 指定要排除的文件/目录
exclude: ["/node_modules/"],
// 默认情况下启用缓存以减少执行时间
cache: false,
// 默认值true,将始终发出发现的错误,禁用设置为false
emitError: true,
// 默认值true,将始终发出找到的警告,以禁用设置为false
emitWarning: true,
// 默认值true,如果出现任何错误,将导致模块构建失败,禁用设置为false
failOnError: failOnError,
// 默认值false,如果出现任何警告,将导致模块构建失败,那么设置为true
failOnWarning: false,
/** 将错误的输出写入文件,例如用于报告 Jenkins CI 的 checkstyle xml 文件。
* outputReport = boolean | { filePath?: string | undefined, formatter?: function | undefined}
* 默认值false
* 是绝对路径或相对于 webpack 配置的路径。
*/
outputReport: false
})
}
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
const config = require("./config");
const utils = require("./utils");
module.exports = {
context: path.resolve(__dirname, "../"),
entry: {
app: "./src/main.js"
},
output: {
path: config.build.assetsRoot,
filename: "[name].js"
},
resolve: {
extensions: [".js", ".vue", ".json"],
mainFields: ["main"],
alias: {
vue$: "vue/dist/vue.esm.js",
"@": utils.resolve("src")
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: "vue-loader"
},
{
test: /\.js$/,
include: [utils.resolve("src")],
use: [
{
loader: "thread-loader",
options: {
workers: 3
}
},
"babel-loader"
]
},
{
test: /\.(png|jpe?g|gif|svg)$/,
use: [
{
loader: "url-loader",
options: {
esModule: false,
limit: 10000,
name: utils.assetsPath("img/[name].[hash:8].[ext]")
}
}
]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/,
loader: "url-loader",
options: {
esModule: false,
limit: 10000,
name: utils.assetsPath("media/[name].[hash:8].[ext]")
}
},
{
test: /\.(woff|woff2|eot|otf|ttf)$/,
use: [
{
loader: "url-loader",
options: {
esModule: false,
limit: 10000,
name: utils.assetsPath("fonts/[name].[hash:8].[ext]")
}
}
]
}
]
},
plugins: [
// 忽略moment插件非本地语言的其他代码
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new VueLoaderPlugin(),
// 抽离css文件
new MiniCssExtractPlugin({
filename: utils.assetsPath("css/[name]_[contenthash:8].css")
}),
// 优化构建日志插件
new FriendlyErrorsWebpackPlugin(),
function errorPlugin() {
this.hooks.done.tap("done", (stats) => {
if (
stats.compilation.errors
&& stats.compilation.errors.length
&& process.argv.indexOf("--watch") === -1
) {
// 1表示错误码并退出
process.exit(1);
}
});
},
// 清理构建目录
new CleanWebpackPlugin(),
// eslint配置
...(config[utils.getEnvName()].useEslint ? [utils.createEsLintPlugin(config[utils.getEnvName()].esLintFailOnError)] : [])
],
stats: "errors-only"
};
webpack.dev.js
// 设置环境
process.env.NODE_ENV = "development";
const path = require("path");
const { merge } = require("webpack-merge");
const utils = require("./utils");
const config = require("./config");
const webpack = require("webpack");
const portfinder = require("portfinder");
const baseConfig = require("./webpack.base");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
const notifier = require("node-notifier");
const env = utils.getEnvironment();
const HOST = process.env.HOST;
const PORT = process.env.PORT && Number(process.env.PORT);
const devConfig = {
mode: "development",
output: {
publicPath: config.dev.assetsPublicPath
},
devServer: {
headers: {"Access-Control-Allow-Origin": "*"},
historyApiFallback: {
rewrites: [
{
from: /.*/,
to: path.posix.join(config.dev.assetsPublicPath, "index.html")
}
]
},
static: {
directory: config.build.assetsRoot,
publicPath: "/"
},
hot: true,
compress: true,
client: {
// 关闭webpack-dev-server客户端的日志输出
logging: "none",
overlay: {
// compilation errors
errors: true,
// compilation warnings
warnings: false,
// unhandled runtime errors
runtimeErrors: false
},
// 显示打包进度
progress: true
},
host: HOST || config.dev.host,
port: PORT || config.dev.port,
proxy: config.dev.proxy,
open: config.dev.open
},
devtool: config.dev.devtool,
// stats构建日志输出配置几种配置说明
// 1. false: 什么都不输出
// 2. errors-only: 只在发生错误时输出
// 3. minimal: 只在发生错误或有新的编译时输出
// 4. none: 没有输出
// 5. normal: 标准输出
// 6. verbose: 全部输出
stats: "errors-only",
module: {
rules: utils.cssLoaders({
sourceMap: config.dev.cssSourceMap,
extract: true,
usePostCSS: true
})
},
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin({
test: /\.css$/i
})
]
},
plugins: [
// 注册全局变量
new webpack.DefinePlugin({
"process.env": env
}),
// 热更新插件,结合webpack-dev-server使用
new webpack.HotModuleReplacementPlugin(),
// 页面模块
new HtmlWebpackPlugin({
template: "index.html",
filename: 'index.html',
inject: true
}),
// 将static文件夹下的静态资源复制到dist/static文件夹下
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
globOptions: {
ignore: ['.*'],
},
},
],
}),
]
};
module.exports = new Promise((resolve, reject) => {
portfinder.setBasePort(process.env.PORT || config.dev.port);
portfinder.getPort(function (err, port) {
//
// `port` is guaranteed to be a free port
// in this scope.
//
if (err) {
reject(err);
} else {
process.env.PORT = port;
devConfig.devServer.port = port;
devConfig.plugins.push(
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: [
`You application is running here http://${devConfig.devServer.host}:${port}`
]
},
onErrors: (severity, errors) => {
if (severity !== "error") {
return;
}
const error = errors[0];
notifier.notify({
title: "Webpack error",
message: severity + ": " + error.name,
subtitle: error.file || "",
icon: path.join(__dirname, "icon.png")
});
}
})
);
resolve(merge(baseConfig, devConfig));
}
});
});
webpack.dll.js
const path = require("path");
const webpack = require("webpack");
const utils = require("./utils");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
module.exports = {
mode: "production",
entry: {
library: ["vue/dist/vue.esm.js", "element-ui"],
vendors: ["axios", "vue-router/dist/vue-router.common.js"]
},
output: {
filename: "dll.[name].min.js",
path: utils.resolve("./app-dll"),
library: "[name]", // 保持与webpack.DllPlugin中name一致
libraryTarget: 'window'
},
resolve: {
alias: {
vue$: "vue/dist/vue.esm.js"
}
},
plugins: [
new ProgressBarPlugin(),
// 清理构建目录
new CleanWebpackPlugin(),
new webpack.DllPlugin({
name: "[name]", // 保持与output.library中名称一致
path: utils.resolve("./app-dll/manifest.[name].json")
}),
new BundleAnalyzerPlugin()
],
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
format: {
// 删除注释
comments: true
}
},
// 是否将注释剥离到单独的文件中
extractComments: false
})
]
}
};
webpack.prod.js
// 设置环境
process.env.NODE_ENV = "production";
const { merge } = require("webpack-merge");
const webpack = require("webpack");
const path = require("path");
const fs = require("fs");
const glob = require("glob");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const PurgeCSSPlugin = require("purgecss-webpack-plugin");
const AddAssetHtmlPlugin = require("add-asset-html-webpack-plugin");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const cssnano = require("cssnano");
const config = require("./config");
const utils = require("./utils");
const baseConfig = require("./webpack.base.js");
const webpackDll = require("./webpack.dll.js");
const env = utils.getEnvironment();
const prodConfig = {
mode: "production",
output: {
filename: utils.assetsPath("js/[name].[chunkhash:8].js"),
publicPath: config.build.assetsPublicPath
},
module: {
rules: utils.cssLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
plugins: [
new ProgressBarPlugin(),
new webpack.DefinePlugin({
"process.env": env
}),
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: cssnano
}),
// 页面模块
new HtmlWebpackPlugin({
template: "index.html",
filename: `${config.build.assetsRoot}/index.html`,
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
}),
// 将static文件夹下的静态资源复制到dist/static文件夹下
// new CopyWebpackPlugin({
// patterns: [
// {
// from: path.resolve(__dirname, '../static'),
// to: config.build.assetsSubDirectory,
// globOptions: {
// ignore: ['.*'],
// },
// },
// ],
// }),
// 生成git版本信息
function gitVersionPlugin() {
this.hooks.done.tap("done", () => {
if (!fs.existsSync(config.build.assetsRoot)) {
fs.mkdirSync(config.build.assetsRoot);
}
const verisionJson = JSON.stringify(
{ version: env.GITVERSION, branch: env.GITBRANCH },
null,
2
);
fs.writeFile(
path.join(config.build.assetsRoot, "version.json"),
verisionJson,
(err) => {
if (err) {
console.log("版本JSON创建失败");
} else {
console.log("版本JSON创建成功");
}
}
);
});
}
],
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
// 过滤掉以".min.js"结尾的文件.
exclude: /\.min\.js$/i,
// Enable multi-process parallel running and set number of concurrent runs.
parallel: true,
// Enable file caching. Default path to cache directory: node_modules/.cache/terser-webpack-plugin.
cache: true,
terserOptions: {
// 开启变量名混淆
mangle: true,
compress: {
unused: true,
// 移除所有debugger
drop_debugger: true,
// 移除所有console
drop_console: true,
pure_funcs: [
// 移除指定的指令,如console, alert等
"console.log",
"console.error",
"console.dir"
]
},
format: {
// 删除注释
comments: true
}
},
// 是否将注释剥离到单独的文件中
extractComments: false
})
],
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: "commons",
chunks: "all",
minChunks: 3
}
}
}
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
performance: {
// 用于控制Webpack在资源的大小超过限制的时候,做出提示
hints: false
}
};
// 通过webpack.dll.js中的entry获取包名,然后引用manifest.json和dll.js
Object.keys(webpackDll.entry).forEach((name) => {
// 引用 manifest.json
prodConfig.plugins.push(
new webpack.DllReferencePlugin({
manifest: require(`../app-dll/manifest.${name}.json`)
})
);
// Add a JavaScript or CSS asset to the HTML generated by html-webpack-plugin
prodConfig.plugins.push(
new AddAssetHtmlPlugin([
{
filepath: utils.resolve(`./app-dll/dll.${name}.min.js`),
outputPath: "./static/app-dll",
publicPath: "./static/app-dll"
}
])
);
});
// 是否启用剔除多余不用的CSS样式
if(config.build.purgeCSS) {
const purgeCSSPlugin = new PurgeCSSPlugin({
paths: glob.sync(`${utils.resolve("src")}/**/*`, { nodir: true }),
only: ["app"],
// safelist 参考地址:https://purgecss.com/safelisting.html
// standard: selectors width el such as el-button
// deep: selectors width el as well as their children such as el-table th
// greedy:selectors that have any part contain el such as button.el-button
safelist: {
standard: [
/^el-/,
/-(leave|enter|appear)(|-(to|from|active))$/,
/^(?!(|.*?:)cursor-move).+-move$/,
/^router-link(|-exact)-active$/,
/data-v-.*/,
"html",
"body"
],
deep: [/^el-/],
greedy: [/^el-/]
}
});
prodConfig.plugins.push(purgeCSSPlugin)
}
// 是否开启打包后的分析报告
if (config.build.bundleAnalyzerReport) {
prodConfig.plugins.push(new BundleAnalyzerPlugin());
}
let webpackConfig = merge(baseConfig, prodConfig);
// 是否需要测量webpack构建速度
if (config.build.isSpeedMeasure) {
const smp = new SpeedMeasurePlugin();
webpackConfig = smp.wrap(webpackConfig);
}
module.exports = webpackConfig;
.babelrc
{
"presets": [["@babel/preset-env"], "@vue/babel-preset-jsx"],
"plugins": ["@babel/plugin-syntax-dynamic-import"],
"compact": false
}
.env.development
NODE_ENV=development
BASE_API=/base
.env.production
NODE_ENV=production
BASE_API=/base
.eslintrc
const webpackBaseConfig = require('./app-build/webpack.base.js');
module.exports = {
/** 根目录标识
* 标识当前配置文件为eslint的根配置文件,停止在父级目录中查找
*/
root: true,
/** 解析器
* ESLint 默认使用Espree作为其解析器
* 解析器必须是本地安装的一个 npm 模块。即必须按照在本地的node_module中。
* 解析器是用于解析js代码的,会对js进行一些语法分析,语义分析什么的,才能判断语句符不符合规范。
* 解析器有很多,但兼容eslint的解析器有以下几个:
* Espree:默认解析器,一个从Esprima中分离出来的解析器,做了一些优化
* Esprima:js标准解析器,是一个用来从字符串中解析js代码的工具
* Babel-ESLint:一个对Babel解析器的包装,使其能够与ESLint兼容。如果我们的代码需要经过babel转化,则对应使用这个解析器
* 由于解析器只有一个,用了「vue-eslint-parser」就不能用「babel-eslint」。
* 所以「vue-eslint-parser」的做法是,在解析器选项中,再传入一个解析器选项parser。从而在内部处理「babel-eslint」,检测<script>中的js代码
*/
parser: 'vue-eslint-parser',
/** 解析器选项
* http://eslint.cn/docs/user-guide/configuring#specifying-parser-options
* 有些解析器支持一些特定的选项。你可以使用 parserOptions 来指定不同解析器的选项。
* 注意,在使用自定义解析器时,为了让 ESLint 在处理非 ECMAScript 5 特性时正常工作,配置属性 parserOptions 仍然是必须的。
* 解析器会被传入 parserOptions,但是不一定会使用它们来决定功能特性的开关。。
*
*/
parserOptions: {
parser: 'babel-eslint'
},
/** 运行环境
* http://eslint.cn/docs/user-guide/configuring#specifying-environments
* 常见的运行环境:
* browser - 浏览器环境中的全局变量。
* node - Node.js 全局变量和 Node.js 作用域。
* commonjs - CommonJS 全局变量和 CommonJS 作用域 (仅为使用 Browserify/WebPack 写的只支持浏览器的代码)。
* es6 - 启用除了 modules 以外的所有 ECMAScript 6 特性(该选项会自动设置 ecmaVersion 解析器选项为 6)。
* worker - Web Workers 全局变量。
* amd - 将 require() 和 define() 定义为像 amd 一样的全局变量。
* serviceworker - Service Worker 全局变量。
*/
env: {
node: true,
browser: true,
es6: true
},
/** 插件
* http://eslint.cn/docs/user-guide/configuring#configuring-plugins
* 1、注意插件名忽略了「eslint-plugin-」前缀,所以在package.json中,对应的项目名是「eslint-plugin-vue」
* 2、插件的作用类似于解析器,用以扩展解析器的功能,用于检测非常规的js代码。也可能会新增一些特定的规则。
* 3、如 eslint-plugin-vue,是为了帮助我们检测.vue文件中 <template> 和 <script> 中的js代码
* 4、eslint-plugin-vue 插件依赖 「vue-eslint-parser」解析器。
*/
plugins: ['vue'],
/** 规则继承
* http://eslint.cn/docs/user-guide/configuring#extending-configuration-files
* 可继承的方式有以下几种:
* eslint内置推荐规则,就只有一个,即「eslint:recommended」
* 可共享的配置, 是一个 npm 包,它输出一个配置对象。即通过npm安装到node_module中
* 可共享的配置可以省略包名的前缀 eslint-config-,即实际设置安装的包名是 eslint-config-airbnb-base
* 从插件中获取的规则,书写规则为 「plugin:插件包名/配置名」,其中插件包名也是可以忽略「eslint-plugin-」前缀。如'plugin:vue/essential'
* 从配置文件中继承,即继承另外的一个配置文件,如'./node_modules/coding-standard/eslintDefaults.js'
*/
extends: [
'plugin:vue/essential',
/**
* 有两种eslint规范:
* 一种是自带了react插件的「eslint-config-airbnb」,
* 一种是基础款「eslint-config-airbnb-base」
* 在使用airbnb-base的时候,需要安装「eslint-plugin-import」
* airbnb-base 包括了ES6的语法检测,需要依赖 「eslint-plugin-import」
* airbnb-base 地址:https://github.com/airbnb/javascript
*/
'airbnb-base'
],
/** 规则共享参数
* http://eslint.cn/docs/user-guide/configuring#adding-shared-settings
* ESLint 支持在配置文件添加共享设置。
* 你可以添加 settings 对象到配置文件,它将提供给每一个将被执行的规则。
* 如果你想添加的自定义规则而且使它们可以访问到相同的信息,这将会很有用,并且很容易配置。
*/
settings: {
// 注意,「import/resolver」并不是eslint规则项,与rules中的「import/extensions」不同,它不是规则项.
// 这里只是一个参数名,叫「import/resolver」,会传递给每个规则项。
// settings并没有具体的书写规则,「import/」只是import模块自己起的名字,原则上,它直接命名为「resolver」也可以,不是强制设置的。
// 因为「import」插件很多规则项都用的这个配置项,所以并没有通过rules设置,而是通过settings共享。
// 具体使用方法可参考:https://github.com/benmosher/eslint-plugin-import
'import/resolver': {
/**
* 这里传入webpack并不是import插件能识别webpack,而且通过npm安装了「eslint-import-resolver-webpack」
* import」插件通过「eslint-import-resolver-」+「webpack」找到该插件并使用,就能解析webpack配置项,使用里面的参数。
* 主要是使用以下这些参数,共享给import规则,让其正确识别import路径
* extensions: [".js", ".vue", ".json"],
* alias: {
* vue$: "vue/dist/vue.esm.js",
* "@": utils.resolve("src")
* }
* eslint-import-resolver-webpack参考地址:https://www.npmjs.com/package/eslint-import-resolver-webpack
*/
webpack: {
config : {
resolve: webpackBaseConfig.resolve
}
}
}
},
/** 全局变量
* http://eslint.cn/docs/user-guide/configuring#specifying-globals
* 当访问当前源文件内未定义的变量时,no-undef 规则将发出警告。
* 如果你想在一个源文件里使用全局变量,推荐你在 ESLint 中定义这些全局变量,这样 ESLint 就不会发出警告了。你可以使用注释或在配置文件中定义全局变量。
* key值就是额外添加的全局变量
* value值用于标识该变量能否被重写,类似于const的作用。writable为允许变量被重写, readonly不允许被重写。
* 注意:要启用no-global-assign规则来禁止对只读的全局变量进行修改。
*/
globals: {
// var1: "writable" // 例如定义var1这个全局变量,且这个变量可以被重写
// var2: "readonly" // 例如定义var2这个全局变量,但是这个变量不可以被重写
},
/** 自定义规则
* http://eslint.cn/docs/user-guide/configuring#configuring-rules
* "off" 或者0 关闭规则
* "warn" 或者1 将规则打开为警告(不影响退出代码)
* "error" 或者2 将规则打开为错误(触发时退出代码为1)
* 示例:如:'no-restricted-syntax': 0, // 表示关闭该规则
* 如果某项规则,有额外的选项,可以通过数组进行传递,而数组的第一位必须是错误级别。
* 如 'semi': ['error', 'never'], never就是额外的配置项
*/
rules: {
// eslint规则参考:https://zh-hans.eslint.org/docs/latest/rules/
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'linebreak-style': 'off', // 取消换行符\n或\r\n的验证
// airbnb-base规则参考:
'prefer-const': 'error', // 2.1 要求声明后永远不会重新赋值的变量使用const
'no-const-assign': 'error', // 2.1 禁止重新分配变量const
'no-var': 'error', // 2.2 要求使用let或const而不是var
'no-new-object': 'error', // 3.1 禁止使用Object构造函数
'object-shorthand': 'error', // 3.2 使用对象方法的简写方式
'quote-props': 'error', // 3.6 Only quote properties that are invalid identifiers
'no-prototype-builtins': 'error', // 3.7 不要直接使用 Object.prototype 的方法,使用类似Object.prototype.hasOwnProperty.call(object, key)
'prefer-object-spread': 'error', // 3.8 使用对象扩展而不是 Object.assign
'no-array-constructor': 'error', // 4.1 禁止使用Array构造函数,使用字面量值创建数组
'array-callback-return': 'error', // 4.7 在数组方法回调中使用 return 语句,如果是单一声明语句的情况,可省略return
'prefer-destructuring': 'error', // 5.1 数组和对象解构
quotes: ['error', 'single'], // 6.1 字符串使用单引号
'prefer-template': 'error', // 6.3 使用模板而非字符串连接
'template-curly-spacing': 'error', // 6.3 模板字符串中的嵌入表达式周围不能使用空格
'no-eval': 'error', // 6.4 禁止使用eval()
'no-useless-escape': 'error', // 6.5 禁止不必要的转义
'func-style': 'error', // 7.1 使用命名函数表达式而不是函数声明
'func-names': 'error', // 7.1 函数表达式必须有名字
'wrap-iife': ['error', 'any'], // 7.2 用圆括号包裹自执行匿名函数,outside、inside、any
'no-loop-func': 'error', // 7.3 不要在非函数代码块(if、while等循环语句中)中声明函数
'prefer-rest-params': 'error', // 7.6 使用剩余运算符rest而不是 arguments
'default-param-last': 'error', // 7.9 把默认参数赋值放在最后
'no-new-func': 'error', // 7.10 禁止使用Function构造函数
'space-before-function-paren': 'error', // 7.11 函数名称或关键字与左括号之间需要加空格
'space-before-blocks': 'error', // 7.11 在块级作用域之前需要加空格
'no-param-reassign': 'error', // 7.12 禁止对函数参数再赋值
'prefer-spread': 'error', // 7.14 使用扩展运算符而非.apply
'function-paren-newline': 'error', // 7.15 在函数括号内强制使用一致的换行符
'prefer-arrow-callback': 'error', // 8.1 使用箭头函数作为回调
'arrow-spacing': 'error', // 8.1 箭头函数中强制调整箭头前后的间距一致
'arrow-parens': 'error', // 8.2 箭头函数中强制使用圆括号
'arrow-body-style': 'error', // 8.2 如果函数体只包含一条没有副作用的返回表达式的语句,可以省略花括号并使用隐式的return, 否则保留花括号并使用return语句
'no-confusing-arrow': 'error', // 8.5 避免箭头函数(=>)和比较操作符(<=, >=)混淆使用
'implicit-arrow-linebreak': 'error', // 8.6 使用隐式返回强制箭头函数体的位置
'no-useless-constructor': 'error', // 9.5 如果未指定默认构造函数,则类具有默认构造函数。不需要空构造函数或仅委托给父类的构造函数。
'class-methods-use-this': 'error', // 9.7 除非外部库或框架要求使用特定的非静态方法,否则类方法应使用此方法或将其制成静态方法。
'no-duplicate-imports': 'error', // 10.4 禁止重复导入
'import/no-mutable-exports': 'error', // 10.5 禁止导出可变的引用
'import/prefer-default-export': 'error', // 10.6 当模块只有一个导出时,更喜欢使用默认导出而不是命名导出。
'import/first': 'error', // 10.7 强制将导入放在顶层
'object-curly-newline': 'error', // 10.8 多行import应该缩进,就像多行数组和对象字面量
'import/no-webpack-loader-syntax': 'error', // 10.9 禁止使用 Webpack loader 语法
'import/extensions': [
'error',
'always',
{
js: 'never',
vue: 'never'
}
], // 10.10 文件扩展名
'no-iterator': 'error', // 11.1 禁止使用迭代器
'no-restricted-syntax': 'error', // 11.1 禁止指定的语法
'generator-star-spacing': ['error', { before: false, after: true }], // 11.2 强制 generator 函数中 * 号周围使用一致的空格
'dot-notation': 'error', // 12.1 使用点号访问属性
'prefer-exponentiation-operator': 'error', // 12.3 使用指数运算符 ** 替代 Math.pow
'no-undef': 'error', // 13.1 禁止使用未定义的变量
'one-var': 'error', // 13.2 每个变量都用一个 const 或 let
'no-multi-assign': 'error', // 13.5 变量不要进行链式赋值
'no-plusplus': 'error', // 13.6 禁止使用一元操作符 ++ 和 --
'operator-linebreak': 'error', // 13.7 在赋值的时候避免在 = 前/后换行。如果你的赋值语句超出 max-len, 那就用小括号把这个值包起来再换行。
'no-unused-vars': 'error', // 13.8 不允许未使用的变量
'no-use-before-define': 'error', // 14.5 禁止变量、类和函数在定义之前使用
eqeqeq: 'error', // 15.1 使用 === 和 !== 代替 == 和 !=
'no-case-declarations': 'error', // 15.5 在case和default分句里用大括号创建一块包含语法声明的区域
'no-nested-ternary': 'error', // 15.6 避免嵌套三元表达式
'no-unneeded-ternary': 'error', // 15.7 避免不必要的三元表达式
'no-mixed-operators': 'error', // 15.8 用圆括号来包裹混合操作符
'nonblock-statement-body-position': 'error', // 16.1 用大括号包裹多行代码块
'brace-style': 'error', // 16.2 if表达式的else和if的关闭大括号在一行
'no-else-return': 'error', // 16.3 如果if语句有一个return语句,else就不用写了
'spaced-comment': 'error', // 18.3 注释前加空格
indent: ['error', 'tab'], // 19.1 使用设置为 tab键的缩进
'keyword-spacing': 'error', // 19.3 在控制语句(if, while 等)的圆括号前空一格。在函数调用和定义时,参数列表和函数名之间不空格
'space-infix-ops': 'error', // 19.4 用空格来隔开操作符
'eol-last': 'error', // 19.5 文件结尾带单个换行符
'newline-per-chained-call': 'error', // 19.6 方法链中每次调用后需要换行符
'no-whitespace-before-property': 'error', // 19.6 禁止属性前有空白
'padded-blocks': 'error', // 19.8 不要用空行填充块
'no-multiple-empty-lines': ['error', { max: 2 }], // 19.9 不要使用多个空行来填充代码(默认最大连续空行数为2)
'space-in-parens': 'error', // 19.10 圆括号内不要加空格
'array-bracket-spacing': ['error', 'never'], // 19.11 never(默认值)不允许数组括号内有空格
'object-curly-spacing': 'error', // 19.12 大括号内添加空格(强制在大括号内保持一致的间距)
'max-len': ['error', { code: 200 }], // 19.13 限制一行的最大长度
'block-spacing': 'error', // 19.14 作为语句的花括号内也要加空格 —— { 后和 } 前都需要空格
'comma-spacing': 'error', // 19.15 禁止在逗号前使用空格,并要在逗号后使用空格
'computed-property-spacing': 'error', // 19.16 在计算属性括号内强制使用间距
'func-call-spacing': 'error', // 19.17 避免在函数及其调用之间使用空格
'key-spacing': 'error', // 19.18 在对象属性中强制键和值之间的一致间距
'no-trailing-spaces': 'error', // 19.19 禁止行尾空格(空格、制表符和其他 Unicode 空格字符)
'comma-style': 'error', // 20.1 不要前置逗号
'comma-dangle': ['error', 'never'], // 20.2 附加尾随逗号,"never"(默认值)不允许尾随逗号;"always"需要尾随逗号
semi: 'error', // 21.1 语句强制分号结尾
'no-new-wrappers': 'error', // 22.2 禁止使用new创建String, Number, and Boolean实例
radix: 'error', // 22.3 使用parseInt时始终指定基数
'id-length': ['error', { properties: 'never' }], // 23.1 变量名长度,避免使用单个字母命名,让你的命名可描述 { properties: 'never' }表示不检查属性名长度
camelcase: 'error', // 23.2 命名对象、函数和实例时使用驼峰大小写
'new-cap': 'error', // 23.3 构造函数首字母大写
'no-underscore-dangle': 'error', // 23.4 不要使用尾随下划线或前导下划线
'no-restricted-globals': 'error' // 29.1 禁止指定的全局变量
}
};
.prettierrc
{
"printWidth": 200,
"tabWidth": 2,
"semi": true,
"endOfLine": "auto",
"trailingComma": "none"
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>webpack4.x+vue2.x脚手架</title>
<%= require('raw-loader!./meta.html') %>
</head>
<body>
<div id="root-app"></div>
<script type="text/javascript" src="./static/index.js"></script>
</body>
</html>
meta.html
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Cache" content="no-cache">
package.json
{
"name": "itrus-webpack4-vue-cli",
"version": "1.0.0",
"description": "webpack4.x-vue脚手架",
"main": "src/main.js",
"scripts": {
"dev": "webpack-dev-server --progress --config app-build/webpack.dev.js",
"build": "webpack --config app-build/webpack.prod.js",
"dll": "webpack --config app-build/webpack.dll.js",
"lint": "cross-env NODE_ENV=development eslint --ext .js,.vue src --fix"
},
"repository": {
"type": "git",
"url": "https://git.itrus.com.cn/RDCenter/FDD/webengineering/itrus-webpack4-vue-cli.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.5.0",
"element-ui": "^2.15.14",
"vue": "^2.7.14",
"vue-router": "^3.6.5",
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/core": "^7.22.15",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.22.15",
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0",
"@vue/babel-preset-jsx": "^1.4.0",
"add-asset-html-webpack-plugin": "^3.2.2",
"autoprefixer": "^10.4.15",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.3.0",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.4.1",
"core-js": "^3.26.1",
"cross-env": "^7.0.3",
"css-loader": "^4.3.0",
"css-minimizer-webpack-plugin": "^1.3.0",
"cssnano": "^4.1.11",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-import-resolver-webpack": "^0.13.7",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-vue": "^7.18.0",
"eslint-webpack-plugin": "^2.7.0",
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"git-revision-webpack-plugin": "^3.0.6",
"glob": "^7.2.3",
"html-webpack-plugin": "^4.5.2",
"less": "^4.2.0",
"less-loader": "^6.2.0",
"mini-css-extract-plugin": "^1.6.2",
"node-notifier": "^10.0.1",
"optimize-css-assets-webpack-plugin": "^5.0.8",
"portfinder": "^1.0.32",
"postcss": "^8.4.29",
"postcss-loader": "^4.3.0",
"prettier": "^2.8.8",
"progress-bar-webpack-plugin": "^2.1.0",
"purgecss-webpack-plugin": "^4.1.3",
"raw-loader": "^0.5.1",
"sass": "1.32.13",
"sass-loader": "^7.3.1",
"sass-resources-loader": "^2.2.4",
"speed-measure-webpack-plugin": "^1.5.0",
"style-loader": "^1.3.0",
"terser-webpack-plugin": "^4.2.3",
"thread-loader": "^3.0.0",
"url-loader": "^4.1.1",
"vue-eslint-parser": "^7.11.0",
"vue-loader": "^15.10.2",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.7.14",
"webpack": "^4.47.0",
"webpack-bundle-analyzer": "^4.9.1",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.9.0"
},
"eslintIgnore": [
"/app-build",
"/app-dll"
]
}