【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 类型:
image

一般开发环境配置:

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使用和原理分析

背景:构建后的代码存在⼤量闭包代码

如图所示:
image

这样会导致什么问题?

  • ⼤量作⽤域包裹代码,导致体积增⼤(模块越多越明显)
  • 运⾏代码时创建的函数作⽤域变多,内存开销变⼤

模块转换分析

示例:我们编写了一个模块,代码如下

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"]
}

代码分割的效果如图所示:
image

上面编译的圈红的,如果是动态加载的,那会生成一个以[number]_[chunkhash].js生成的文件名。

在webpack中使用ESLint

行内优秀的eslint规范

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

image

如何优化命令⾏的构建⽇志

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.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"
  ]
}

posted @ 2024-09-15 21:55  风雨后见彩虹  阅读(59)  评论(0编辑  收藏  举报