【webpack4系列】设计可维护的webpack4.x+vue构建配置(终极篇)

news/2024/9/24 3:25:47/

文章目录

    • 构建配置包设计
      • 通过多个配置文件管理不同环境的 webpack 配置
      • 抽离成一个 npm 包统一管理(省略)
      • 通过 webpack-merge 组合配置
    • 功能模块设计
    • 目录结构设计
    • 构建配置插件
      • 安装webpack、webpack-cli
      • 关联HTML插件html-webpack-plugin
      • 解析ES6
      • 解析vue、JSX
      • 解析CSS、Less和Sass
        • 解析CSS
        • 解析Less
        • 解析sass
        • 提取CSS
      • 解析图片和字体
        • 资源解析:解析图片
        • 资源解析:解析字体
        • 资源解析:使用url-loader
      • 热更新:webpack-dev-server
      • 文件指纹策略:chunkhash、contenthash和hash
        • 文件指纹如何生成
        • 文件指纹设置
      • HTML 、CSS和JavaScript代码压缩
        • JS压缩
        • CSS压缩
        • HTML压缩
      • 自动清理构建目录产物
      • PostCSS插件autoprefixer自动补齐CSS3前缀
      • 静态资源内联
      • 使用sourcemap
      • Tree Shaking(摇树优化)的使用和原理分析
        • 基础介绍
        • DCE (Dead code elimination)
        • Tree-shaking 原理
      • Scope Hoisting使用和原理分析
        • 背景:构建后的代码存在⼤量闭包代码
        • 模块转换分析
        • 进⼀步分析 webpack 的模块机制
        • scope hoisting 原理
        • scope hoisting 使⽤
      • 代码分割和动态import
        • 代码分割的意义
        • 懒加载 JS 脚本的⽅式
        • 如何使⽤动态 import?
      • 在webpack中使用ESLint
        • 行内优秀的eslint规范
      • 优化构建时命令行的显示日志
        • webpack构建统计信息 stats
        • 如何优化命令⾏的构建⽇志
      • 构建异常和中断处理
      • 速度分析:使用 speed-measure-webpack-plugin
      • 体积分析:使用webpack-bundle-analyzer
      • 多进程/多实例构建
        • 资源并行解析可选方案
        • 使用 HappyPack 解析资源
        • 使用 thread-loader 解析资源
      • 多进程并行压缩代码:terser-webpack-plugin 开启 parallel 参数
      • 进一步分包:预编译资源模块
      • 充分利用缓存提升二次构建速度
      • 缩小构建目标与减少文件搜索范围
        • 缩小构建目标
        • 减少文件搜索范围
      • 擦除无用的CSS
      • 配置其它插件
    • 实现源码
      • /app-build/
        • config.js
        • app-build/utils.js
        • webpack.base.js
        • webpack.dev.js
        • webpack.dll.js
        • webpack.prod.js
      • .babelrc
      • .env.development
      • .env.production
      • .eslintrc
      • .prettierrc
      • index.html
      • meta.html
      • package.json

构建配置包设计

构建配置管理的可选方案:

  • 通过多个配置文件管理不同环境的构建,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,// 移除所有debuggerdrop_debugger: true,// 移除所有consoledrop_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 cachevar installedModules = {};// The require functionfunction __webpack_require__(moduleId) {// Check if module is in cacheif (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 functionmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);// Flag the module as loadedmodule.l = true;// Return the exports of the modulereturn 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.jsfunction helloworld() {return 'Hello webpack';}// CONCATENATED MODULE: ./common/index.jsfunction common() {return "common module";}// CONCATENATED MODULE: ./src/index/index.jsdocument.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-airbnbeslint-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文件中 和

整体示例代码:

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

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.HOSThost: "localhost",// can be overwritten by process.env.PORT, if port is in use, a free one will be determinedport: 8081,// css source mapcssSourceMap: false,// source map类型devtool: "cheap-source-map",// 是否自动打开浏览器open: true,// 是否添加git信息gitRevision: true,// 是否启用eslintuseEslint: true,// 开启eslint后,检测任何出错是否导致编译失败,true=是,false=否esLintFailOnError: false,},// 生产基础配置build: {// 打包后的文件存放的地方assetsRoot: path.resolve(__dirname, "../dist"),// 静态资源存放的文件夹assetsSubDirectory: "static",// 静态资源引用路径assetsPublicPath: "./",// 是否开启source mapproductionSourceMap: false,// source map类型devtool: "cheap-source-map",// 是否开启打包后的分析报告bundleAnalyzerReport: false,// 测量webpack构建速度isSpeedMeasure: false,// 是否添加git信息gitRevision: true,// 清除多余不用的css样式purgeCSS: true,// 是否启用eslintuseEslint: 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 loaderconst cssLoader = {loader: "css-loader",options: {sourceMap: options.sourceMap,modules: {compileType: 'icss'}}};// postcss loaderconst 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.loaderif (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-pluginreturn new ESLintPlugin({// 自动修复fix: true, // 指定应检查的扩展extensions: ["js", "jsx","vue"], // 指定检测的目录context: exports.resolve("src"), // 指定要排除的文件/目录exclude: ["/node_modules/"], // 默认情况下启用缓存以减少执行时间cache: false,// 默认值true,将始终发出发现的错误,禁用设置为falseemitError: true,// 默认值true,将始终发出找到的警告,以禁用设置为falseemitWarning: true,// 默认值true,如果出现任何错误,将导致模块构建失败,禁用设置为falsefailOnError: failOnError,// 默认值false,如果出现任何警告,将导致模块构建失败,那么设置为truefailOnWarning: 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 errorserrors: true,// compilation warningswarnings: false,// unhandled runtime errorsruntimeErrors: 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,// 移除所有debuggerdrop_debugger: true,// 移除所有consoledrop_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.jsonprodConfig.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-pluginprodConfig.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-buttonsafelist: {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"]
}

http://www.ppmy.cn/news/1529621.html

相关文章

Chrome谷歌浏览器登录账号next无反应

文章目录 问题描述 我们的Chrome浏览器在更新之后&#xff0c;会出现登录谷歌账号的时候&#xff0c;当你输入你的谷歌邮箱之后&#xff0c;点击 n e x t next next,也就是下一步的时候&#xff0c;页面没有反应&#xff0c;也就是没有跳转到输入密码的页面。 分析 根据logs里…

无线感知会议系列【3】【基于WiFi和4G/5G的非接触无线感知:挑战、理论和应用-1】

前言&#xff1a; 2020年北京智源大会 张大庆老师的一个报告 参考链接&#xff1a; 基于WiFi和4G/5G的非接触无线感知&#xff1a;挑战、理论和应用_哔哩哔哩_bilibili 目录&#xff1a; 无线感知简介 无线感知的核心 研究方向 Frsenel 模型 基于Fresnel 感知的应用举例…

table表格,让thead固定,tbody内容滚动,关键是都对齐的纯css写法

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f…

果蔬识别系统架构+流程图

相关文章和代码 果蔬识别系统 果蔬识别系统优化&#xff08;1~5&#xff09; 架构图 流程图 初始化 识别流程 学习流程 同步流程 与初始化类似&#xff0c;只是同步只同步一个storeCode数据 删除数据流程 导入数据

PlantUML的使用以及各种图表示例

文章目录 1.介绍2.支持的 UML 图表3.支持的非 UML 图表4.其他功能5. 使用docker部署使用6. 几种puml示例6.1 APP登录6.2 扫码登录6.3 电商重复支付6.4 项目甘特图 1.介绍 PlantUML是一个通用性很强的工具&#xff0c;可以快速、直接地创建各种图表。利用简单直观的语言&#x…

C++ std::find函数 容器元素查找

简介 std::find函数是C标准库内非常实用的一个函数&#xff0c;主要用于在给定范围内查找某个元素&#xff0c;如果找到该元素&#xff0c;则返回指向该元素的迭代器&#xff1b;如果没有找到&#xff0c;则返回指向范围末尾的迭代器&#xff08;即 end() &#xff09;。 fin…

react hooks--useLayoutEffect

概述 ◼ useLayoutEffect看起来和useEffect非常的相似&#xff0c;事实上他们也只有一点区别而已&#xff1a;  useEffect会在渲染的内容更新到DOM上后执行&#xff0c;不会阻塞DOM的更新&#xff1b;  useLayoutEffect会在渲染的内容更新到DOM上之前执行&#xff0c;会…

数据结构与算法-Trie树添加与搜索

trie树的使用场景 我们若需要制作一个通讯录的软件&#xff0c;使用常规树结构查询的复杂度为O(logn),但trie树的复杂度确与数据多少无关&#xff0c;与单词长度有关&#xff0c;这就大大缩减的查询的时间复杂度。 trie树的基本实现 基础结构 package com.study.trieDemo;i…