学习webpack5

143 阅读8分钟

1、初始化目录

// 初始化package.json
npm init -y
// 安装webpack
npm install webpack webpack-cli -D
  • -D 等价于 --save-dev; 开发环境时所需依赖
  • -S 等价于 --save; 生产环境时所需依赖
// 创建webpack.config.js文件用于编写webpack配置
const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}
// 创建src目录用于存放源代码,src/main.js
console.log('test webpack')
// 修改package.json中scripts字段
{
  ...
  "scripts": {
    "build": "webpack --config webpack.config.js"
  },
  ...
}

在终端输入:npm run build后,就会在根目录里创建一个dist文件夹,里面的bundle.js就是打包的文件。

2、webpack基本概念

  • entry 应用程序打包的一个或者多个起点。

    // 单个入口如上文所示。
    // 多入口如下所示,在src下建立song.js,这时output里的filename就需要写成'[name].js',打包后再dist文件夹下就会有main.js、song.js
    
    entry: {
      index: './src/main.js',
      song: './src/song.js',
    },
    
  • output

    webpack打包后的文件。

    多入口时需要在output里的filename里使用占位符name,其文件的名称就是对应的entry的名称。

  • mode

    指定当前的环境,其值有三种development、production、none,默认为production。

    在项目中,不同的环境对应的常量通常是不同的,可以用cross-env来配置。

    npm install cross-env -D
    

    然后在package.json中有如下设置

    ...
    "scripts": {
    ...
      "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.js",
      "build": "cross-env NODE_ENV=production webpack --config webpack.config.js",
      ...
    },
    ...
    

    但是发现一个问题,在webpack.config.js中可以使用process.env,但是在main.js中就不行,会报process is not defined的错误,是因为webpack5删除了Node模块的兼容,所以需要 npm i process - D,且设置ProvidePlugin,然后才能使用process

      npm i process - D
    
     ...
      new webpack.ProvidePlugin({
        process: 'process/browser',
      }),
     ...
    

    然后建立文件env.js

    // env.js
    const env = process.env.NODE_ENV
    const config = {
      development: {
      	// NODE_ENV: 'development',
      	API: 'www.1111.com',
      	PORT: '9527',
      	HTTP: 'http://0.0.0.0:3000',
      },
      production: {
      	// NODE_ENV: 'production',
      	API: 'www.22222.com',
      	PORT: '3000',
      	HTTP: 'http://0.0.0.0:4000',
      },
    }
    module.exports = config[env]
    
    // webpack.config.js
    ...
    function processEnv() {
      const tempOjb = {}
      const objKeys = Object.keys(envConfig)
      objKeys.forEach(e => {
    	tempOjb[`process.env.${e}`] = JSON.stringify(envConfig[e])
      })
      return tempOjb
    }
    ...
    new webpack.DefinePlugin(processEnv()),
    ...
    
  • 热更新(HMR)

    npm i webpack-dev-server -D
    
    module.exports = {
      ...
      devServer: {
        hot: true, // 开启HMR功能
        open: true,
        port: PORT,
        overlay: {
          warnings: false,
          errors: true,
        },
        proxy: {},
      },
      ...
    }
    

    在rules中设置css时要注意,开发环境使用style-loader(实现了 HMR 接口),当样式改变是会热更新,且运行更快

    isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader,

3、webpack之loader

webpack是默认知道如何打包js文件的,但是对于一些,比如图片,字体图标的模块,webpack就不知道如何打包了,loader就是告诉webpack对某个特定的文件该如何去打包。

下面列举一些常用的loader:

  • file-loader:将文件发送到输出目录

  • url-loader:功能类似于 file-loader,但是在文件大小(单位为字节)低于指定的限制时,将会以base64位图片打包到bundle.js文件里

    // webpack4的写法,需要手动加载file-loader、url-loader
    {
      test: /\.(woff|woff2|eot|ttf|otf)$/,
      use: { 
        loader: 'file-loader',
        options: {
          name: '[name]_[contenthash].[ext]',
          outputPath: 'static/',
        },
      }
    },
    {
      test: /\.(png|jpe?g|gif)$/,
      use: {
        loader: "url-loader",
        options: {
          limit: 5 * 1024, // 小于这个时将会以base64位图片打包到bundle.js文件里, 如果文件体积等于或大于限制,将使用 file-loader 并将所有参数传递给它
          name: 'imgs/[name].[contenthash].[ext]',
          esModule: false,
          outputPath:'static/'
        },
      },
    },
    
    // webpack5的写法,自动集成了file-loader、url-loader,只需要加个type
    // filename中name就是原始名称,contenthash使用的是MD5算法,ext就是后缀
    // webpack三种hash的区别:
    //	hash:修改一个文件,项目下的所有文件名的hash都将改变,意味着整个项目的文件缓存都将失效
    //	chunkhash:chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。这样就会有个问题,只要对应css或则js改变,与其关联的文件hash值也会改变。
    //	contenthash:针对文件内容级别的,只有你自己模块的内容变了,那么hash值才改变
    {
      test: /\.(woff(2)?|eot|ttf|otf)$/,
      type: 'asset/resource', // 与 file-loader 一样。
      generator: {
        filename: 'static/[name]_[contenthash][ext]'
      }
    },
    {
      test: /\.(png|jpe?g|gif|svg)$/,
      type: 'asset', // 与 url-loader 一样。
      generator: {
        filename: 'static/[name]_[contenthash][ext]'
      },
      parser: {
        dataUrlCondition: {
          maxSize: 2 * 1024 // 小于这个时将会以base64位图片打包到bundle.js文件里, 如果文件体积等于或大于限制,将使用 file-loader 并将所有参数传递给它
        }
      }
    },
    
  • css-loader:将多个css文件整合到一起,形成一个css文件。

  • style-loader:整合的css部分挂载到head标签中

  • sass-loader:把sass翻译成css

  • postcss-loader:用来转换CSS的工具,比如自动补全浏览器前缀、自动把px代为转换成rem等

  • babel-loader:把ES6+ 等高级语法转化为浏览器能够解析的低级语法

    npm install @babel/core babel-loader @babel/preset-env -D
    

    然后设置最基本的babel-loader配置,项目就能跑来,但是如果用一些新的api就会报错

    ReferenceError: regeneratorRuntime is not defined
    

    这是由于新的api被 babel 转译之后的代码使用了 regeneratorRuntime 这个变量,但是这个变量在最终的代码里未定义造成的报错。

    babel 在转译的时候,会将源代码分成 syntax 和 api 两部分来处理:

    1、syntax:类似于展开对象、optional chain、let、const 等语法

    2、api:类似于 [1,2,3].includes 等函数、方法

    babel 只负责syntax,babel 把 api 单独放在 polyfill 这个模块处理

    解决办法是使用 polyfill 来处理 api。polyfill 包括两部分:regenerator-runtime 和 core-js。babel 在转译新的api的时候,生成的代码里使用了 regeneratorRuntime 这个变量,而这个变量是放在 regenerator-runtime 这个 polyfill 库中的。@babel/preset-env 中有一个配置选项 useBuiltIns,用来告诉 babel 如何处理 api。由于这个选项默认值为 false,即不处理 api,所以useBuiltIns必须要设置为 usage 或者 entry(usage是按需加载,entry是全部加载)。

    但是Babel 官网上说 polyfill 将来会被废弃,之后的用法如下

    npm install core-js regenerator-runtime -S
    
    // main.js
    import 'core-js/stable'
    import 'regenerator-runtime/runtime'
    ...
    
    // webpack.config.js
    ...
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: 3,
      },
    ],
    ...
    

4、webpack之plugins

可以在webpack运行到某个时刻的时候,帮你做一些事情。包括:打包优化,资源管理,注入环境变量。

下面列举一些常用的plugin:

  • clean-webpack-plugin:每次打包前先清除以前的打包文件。
  • html-webpack-plugin:让打包的JS文件自动的插入到html模板中。

5、webpack之tree shaking

tree shaking的主要作用是消除无用的js代码(剔除模块中没有导出或引用的部分)。

前提:1、使用ES6模块化 2、开启production

// 如果希望某些文件不需要tree shaking,可以在package.json中设置‘sideEffects’属性
{
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}

6、安装vue3

npm install vue@next -S
npm install vue-loader@next @vue/compiler-sfc -D
// webpack.config.js 添加如下
...
const { VueLoaderPlugin } = require('vue-loader/dist/index')
...
  rules: [
    {
  	  test: /\.vue$/,
  	  use: ['vue-loader']
  	}
  ]
...
new VueLoaderPlugin()

7、安装ESLint && prettier

npm install eslint babel-eslint eslint-plugin-vue -D // 安装eslint相关
npm install eslint-config-prettier eslint-plugin-prettier prettier -D // 安装prettier相关

新增.eslintrc.js文件,用来配置ESLint

// 在package.json中设置prettier的配置,因为prettier有可能会与ESLint有冲突
"prettier": {
  "printWidth": 100,
  "tabWidth": 2,
  "jsxSingleQuote": true,
  "vueIndentScriptAndStyle": false,
  "singleQuote": true,
  "semi": false,
  "arrowParens": "avoid"
},

在package.json中设置了prettier后需要把vscode关了再打开,不然会有种prettier不起作用的错觉。

8、webpack 之缓存

在Webpack4中,运行时是有缓存的,只不过缓存只存在于内存中。所以,一旦Webpack的运行程序被关闭,这些缓存就丢失了。这就导致我们npm run start/build的时候根本无缓存可用。4中的解决办法是使用cache-loader和dll(因为cache-loader并不能覆盖所有模块,只能对个别被loader处理的模块进行缓存。而那些通用的库是没法被cache-loader处理的,所以只能通过dll的方式来预编译。)。而webpack5 自带了持久化缓存,配置如下

cache: isDevMode
  ? {
	type: 'memory',
  }
  : {
	type: 'filesystem',
	buildDependencies: {
	  config: [__filename],
	},
  },

9、webpack之外部扩展(Externals)

如果初始化bundle包的体积太大是,可以利用Externals,在运行时获取扩展依赖,从而减小bundle的体积。

// webpack.config.js
module.exports = {
  //...
  externals: {
    'vue': 'Vue',
  },
};
// index.html
<body>
  <div id="app"></div>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.runtime.min.js"></script>
</body>

注意,打包后的dist/index.html里,依赖文件要在app.js的上面

10、webpack之git Hooks

  • 首先配置commitlint,来约束commit messag规范

    npm install --save-dev @commitlint/config-conventional @commitlint/cli
    
    @commitlint/cli:用来检查规范的
    @commitlint/config-conventional:具体是哪种规范
    // 用法(注意这里冒号后面要有空格)
    git commit -m <type>(<scope>?): <subject>
    type(必须):
    	feat:新功能(feature)
    	ci:自动化流程配置修改
    	fix:修补bug
    	docs:文档更新(documentation)
    	style:修改了空格、缩进等(不影响代码运行的变动)
    	refactor:功能重构(即不是新增功能,也不是修改bug的代码变动)
    	test:增加测试
    	chore:构建过程或辅助工具的变动 比如 webpack babel eslint配置
    	perf:优化相关,比如提升性能、体验。
    	revert:回滚
    	chore: 构建/工程依赖/工具
    scope(可选):scope用于说明commit影响的范围,比如feat(src/element.js): element按需导入
    subject(必须):commit的简短描述
    
    // 在项目目录下创建commitlint.config.js配置文件
    module.exports = {
      extends: ['@commitlint/config-conventional'],
      /**
       * git commit -m <type>(<scope>?): <subject>
       * name:[0, 'always', 72]
       * 数组中第一位为level,可选0, 1, 20为disable,1为warning,2error
       * 第二位为应用与否,可选always|never
       * 第三位该rule的值
       */
      rules: {
    	'type-enum': [
    		2,
    		'always',
    		['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore'],
    	],
    	'type-case': [0],
    	'type-empty': [0],
    	'scope-empty': [0],
    	'scope-case': [0],
    	'subject-full-stop': [0, 'never'],
    	'subject-case': [0, 'never'],
    	'header-max-length': [0, 'always', 72],
      },
    }
    
  • 安装husky

    husky在版本6之前和之后的用法完全不一样,下面只谈谈6之后的用法

    npm install -D husky // husky可以让我们向项目中方便添加git hooks。
    
    // 在package.json中增加如下命令
    ...
    "scripts": {
      ...
      "prepare": "husky install"
      ...
    }
    ...
    

    然后会在项目根目录下生成一个.kusky文件夹,在文件夹内创建commit-msg文件,内容如下:

    #!/bin/sh
    
    npx commitlint -e $1
    

    具体可以去官网看。

  • 安装lint-staged

    用来校验你修改的那部分文件,而不是全部文件。

    // 在package.json中增加lint-staged
    ...
    "lint-staged": {
      "src/**/*.{js,jsx,vue,json,css,scss,md,html}": [
         "npm run lint"
      ]
    },
    ...
    

11、vue-cli-service中的webpack配置

上面是webpack的一些基本配置,如何进一步优化,有哪些优化方法?用到哪些loader和plugins?没有头绪。于是想到vue-cli-service也是基于webpack,可以参考它是怎么对webpack封装的。于是在node_modules/@vue/cli-servie/lib/config里找到了相关的webpack配置。还有个比较直接的方法,就是执行如下的代码

vue inspect > output.js

output.js就是webpack配置。

12、webpack之MF(Module Federation)

webpack中的微前端。

// 在vue_app应用中,对webpack.config.js增加如下代码
const ModuleFederationPlugin = require("webpack").container.ModuleFederationPlugin

module.exports = {
  ...
  devServer: {
  	port: '1000',
  	...
  }
  ...
  plugins: [
  	new ModuleFederationPlugin({
      name: 'vue_app', // 导出包的命名空间
      filename: 'vue_app_App.js', // 对外提供打包后的文件名,导入时使用
      exposes:{  // 导出
        './ss': './src/App.vue',
        './about': './src/About.vue',
        './math': './src/math.js',
      },
      shared: { // 打包暴露模块时,不会将 shared 打包,是两个应用的共享依赖,比如 react vue 等
        vue: {
          eager: true,
          singleton: true
        },
      }
    })
  ]
}

然后启动vue_app应用

// 在vue_son应用中,对对webpack.config.js增加如下代码
const ModuleFederationPlugin = require("webpack").container.ModuleFederationPlugin

module.exports = {
  ...
  devServer: {
  	port: '2000',
  	...
  }
  ...
  plugins: [
  	new ModuleFederationPlugin({
      name: 'vue_son', // 导出包的命名空间
      filename: 'vue_son_App.js', // 对外提供打包后的文件名,导入时使用
      exposes:{  // 导出
        './App': './src/App.vue'
      },
      remotes: { // 导入,需要依赖的模块名
        from_App: 'vue_app@http://localhost:1000/vue_app_App.js'
      },
    })
  ]
}
// 用的时候如下,必须要用defineAsyncComponent包一下
<template>
  <div class="app-style">
	<div class="text" :style="{ color: 'green' }">这是vue_son333</div>
	<SonAbout />
	<div>~~~~~~~~~~~~~~~~~~~~~~~app~~~~~~~~~~~~</div>
	<AsyncComp />
	<AsyncAbout :name="'李四'"/>
	<AsyncAbout :name="'王五'"/>
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue'
import SonAbout from './About.vue'
export default {
  name: 'App',
  components: {
	SonAbout,
	AsyncComp: defineAsyncComponent(() => import('from_App/ss')),
	AsyncAbout: defineAsyncComponent(() => import('from_App/about')),
  },
}

引用只能是组件,js文件不行,另外跨框架也不行。

13、具体配置

// webpack.config.js

const path = require('path')
const envConfig = require('./env')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin')
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { VueLoaderPlugin } = require('vue-loader/dist/index')
const TerserPlugin = require('terser-webpack-plugin')
const FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin')

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

function resolve(dir) {
  return path.join(__dirname, dir)
}

function processEnv() {
  const tempOjb = {}
  const objKeys = Object.keys(envConfig)
  objKeys.forEach(e => {
    tempOjb[`process.env.${e}`] = JSON.stringify(envConfig[e])
  })
  return tempOjb
}

const { PORT, API, HTTP } = envConfig
const limitSize = 4 * 1024
const isDevMode = process.env.NODE_ENV === 'development'
console.log(process.env.NODE_ENV)
module.exports = {
  entry: {
    app: ['./src/main.js'],
  },
  output: {
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].js',
    path: resolve('dist'),
  },
  mode: process.env.NODE_ENV,
  devtool: isDevMode ? 'eval-source-map' : false,
  devServer: {
    hot: true, // 开启HMR功能
    open: true,
    port: PORT,
    overlay: {
      warnings: false,
      errors: true,
    },
    clientLogLevel: 'silent', // 关闭日志。
    proxy: {
      [API]: {
        target: HTTP,
        changeOrigin: true,
        pathRewrite: {
          [`^${API}`]: '',
        },
      },
    },
  },
  resolve: {
    alias: {
      '@': resolve('src'),
    },
    extensions: ['.js', '.jsx', '.vue', '.json', '.wasm'], // 按顺序解析这些后缀名。
  },
  // webpack5 自带了持久化缓存
  cache: isDevMode
    ? {
        type: 'memory',
      }
    : {
        type: 'filesystem',
        buildDependencies: {
          config: [__filename],
        },
      },
  optimization: {
    splitChunks: {
      chunks: 'all', // 表明将选择哪些 chunk 进行优化
      minSize: 30000, // 代码最小的体积
      // runtimeChunk: 'single',
      // 缓存
      // cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块,就分配到该组。
      // 模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。
      // 默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
      cacheGroups: {
        vendors: {
          name: `chunk-vendors`,
          test: /[\\/]node_modules[\\/]/, // 匹配node_modules中的模块
          priority: -10, // 权重,数字越大表示优先级越高
          chunks: 'initial',
        },
        Element: {
          name: 'chunk-element-plus', // 把antdUI单独分出一个包
          priority: 20,
          test: /[\\/]node_modules[\\/]_?element-plus(.*)/,
        },
        Echarts: {
          name: 'chunk-echarts', // 把echarts单独分出一个包
          priority: 19,
          test: /[\\/]node_modules[\\/]_?echarts(.*)/,
        },
        default: {
          name: `chunk-default`,
          minChunks: 2, // 表示一个模块至少应被指定个数的 chunk 所共享才能分割。
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true,
        },
      },
    },
    minimize: true, // 告诉webpack使用插件压缩 bundle
    minimizer: [
      // 压缩js代码
      new TerserPlugin({
        terserOptions: {
          compress: {
            // 压缩时的一些优化
            // https://github.com/terser/terser
            arrows: false,
            collapse_vars: false,
            comparisons: false,
            computed_props: false,
            hoist_funs: false,
            hoist_props: false,
            hoist_vars: false,
            inline: false,
            loops: false,
            negate_iife: false,
            properties: false,
            reduce_funcs: false,
            reduce_vars: false,
            switches: false,
            toplevel: false,
            typeofs: false,
            booleans: true,
            if_return: true,
            sequences: true,
            unused: true,
            conditionals: true,
            dead_code: true,
            evaluate: true,
          },
          mangle: {
            safari10: true,
          },
        },
        parallel: true, // 启用/禁用多进程并发运行功能。使用多进程并发运行以提高构建速度。
        extractComments: false, // 启用/禁用剥离注释功能。
      }),
    ],
  },
  target: 'web', // 为webpack指定一个环境,如果不设置则热更新不起作用
  module: {
    // 不去解析属性值代表的库的依赖,增加打包速度
    noParse: /^(vue|vue-router|vuex|vuex-router-sync)$/,
    rules: [
      {
        test: /\.(sc|c)ss$/,
        use: [
          // 在开发环境使用style-loader(实现了HMR接口),当样式改变是会热更新,且运行更快
          isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2, // 前面要先走sass-loader和postcss-loader
              modules: true, //  对所有文件启用 CSS 模块
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  'autoprefixer',
                  require('cssnano')({
                    // 压缩css
                    preset: [
                      'default',
                      {
                        mergeLonghand: false, // 不需要折叠
                        cssDeclarationSorter: false, // 不需要排序
                      },
                    ],
                  }),
                ],
              },
            },
          },
          'sass-loader',
        ],
      },
      {
        test: /\.(png|jpe?g|gif|webp|svg)(\?.*)?$/,
        type: 'asset',
        generator: {
          filename: 'static/img/[name]_[contenthash:8][ext]',
        },
        // 小于这个时将会以base64位图片打包到bundle.js文件里, 如果文件体积等于或大于限制,将使用 file-loader 并将所有参数传递给它
        parser: {
          dataUrlCondition: {
            maxSize: limitSize,
          },
        },
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        type: 'asset',
        generator: {
          filename: 'static/media/[name]_[contenthash:8][ext]',
        },
        // 小于这个时将会以base64位图片打包到bundle.js文件里, 如果文件体积等于或大于限制,将使用 file-loader 并将所有参数传递给它
        parser: {
          dataUrlCondition: {
            maxSize: limitSize,
          },
        },
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
        type: 'asset',
        generator: {
          filename: 'static/fonts/[name]_[contenthash:8][ext]',
        },
        // 小于这个时将会以base64位图片打包到bundle.js文件里, 如果文件体积等于或大于限制,将使用 file-loader 并将所有参数传递给它
        parser: {
          dataUrlCondition: {
            maxSize: limitSize,
          },
        },
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              [
                '@babel/preset-env',
                {
                  useBuiltIns: 'usage', // 如果没有设置,就不对最新你的api进行处理。usage是按需加载,entry是全部加载
                  corejs: 3,
                },
              ],
            ],
          },
        },
      },
      {
        test: /\.vue$/,
        use: [
          // 'cache-loader', // 在一些性能开销较大的 loader 之前添加 cache-loader,以便将结果缓存到磁盘里。
          {
            loader: 'vue-loader',
            options: {
              compilerOptions: {
                whitespace: 'condense', // preserve是保留HTML空格与换行,condense是压缩HTML空格与换行 ?
                preserveWhitespace: true, // vue去掉元素之间的空格,减小打包体积
              },
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    // webpack5把node.js的polyfills删除了,
    // 所以需要 npm i process - D,且设置ProvidePlugin,然后才能使用process
    new webpack.ProvidePlugin({
      process: 'process/browser',
    }),
    // 强制所有必需模块的完整路径与磁盘上实际路径的确切大小写相匹配
    new CaseSensitivePathsPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      title: 'webpack + Vue3',
      minify: {
        collapseWhitespace: false, // 去掉空格
        removeComments: true, // 去掉注释
        removeAttributeQuotes: true, // 去掉属性引用
        collapseBooleanAttributes: true, // 是否简写boolean格式的属性如:disabled="disabled" 简写为disabled  默认false
        removeScriptTypeAttributes: true, // 删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false
      },
      inject: 'body',
    }),
    // 设置预加载
    // preload-webpack-plugin会报错,需要使用@vue/preload-webpack-plugin
    new PreloadWebpackPlugin({
      rel: 'preload',
      // include: 'initial', // 仅预加载初始块
      include: {
        type: 'initial',
        entries: ['app'],
      },
      fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], // 不想预加载的块,黑名单
    }),
    // new PreloadWebpackPlugin({
    // 	rel: 'prefetch',
    // 	include: 'asyncChunks',
    // }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
    }),
    new VueLoaderPlugin(),
    new webpack.DefinePlugin(processEnv()), // 设置全局常量
    new BundleAnalyzerPlugin({
      analyzerHost: '127.0.0.2',
      analyzerPort: 9528,
    }), // 打包大小的分许工具,stat文件开始的大小,parsed是webpack压缩后的大小
    new FriendlyErrorsWebpackPlugin(), // webpack错误日志
  ],
}

// package.json

{
  "name": "webpack5_vue3",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev:lint": "npm run dev && npm run lint",
    "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.js",
    "build": "cross-env NODE_ENV=production webpack --config webpack.config.js",
    "lint": "eslint --fix --ext .js,.jsx,.vue src",
    "prepare": "husky install"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.14.8",
    "@babel/preset-env": "^7.14.9",
    "@commitlint/cli": "^13.1.0",
    "@commitlint/config-conventional": "^13.1.0",
    "@soda/friendly-errors-webpack-plugin": "^1.8.0",
    "@vue/compiler-sfc": "^3.1.4",
    "@vue/preload-webpack-plugin": "^2.0.0",
    "autoprefixer": "^10.2.6",
    "babel-eslint": "^10.1.0",
    "babel-loader": "^8.2.2",
    "case-sensitive-paths-webpack-plugin": "^2.4.0",
    "clean-webpack-plugin": "^4.0.0-alpha.0",
    "cross-env": "^7.0.3",
    "css-loader": "^5.2.6",
    "cssnano": "^5.0.6",
    "eslint": "^7.30.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^3.4.0",
    "eslint-plugin-vue": "^7.13.0",
    "html-webpack-plugin": "^5.3.2",
    "husky": "^7.0.1",
    "mini-css-extract-plugin": "^2.1.0",
    "postcss-loader": "^6.1.1",
    "prettier": "^2.3.2",
    "process": "^0.11.10",
    "sass": "^1.35.2",
    "sass-loader": "^12.1.0",
    "style-loader": "^3.2.1",
    "terser-webpack-plugin": "^5.1.4",
    "vue-loader": "^16.2.0",
    "webpack": "^5.43.0",
    "webpack-bundle-analyzer": "^4.4.2",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2"
  },
  "dependencies": {
    "core-js": "^3.16.1",
    "echarts": "^5.1.2",
    "element-plus": "^1.0.2-beta.70",
    "lodash": "^4.17.21",
    "regenerator-runtime": "^0.13.9",
    "vue": "^3.1.4",
    "vue-router": "^4.0.10"
  },
  "prettier": {
    "printWidth": 100,
    "tabWidth": 2,
    "jsxSingleQuote": true,
    "vueIndentScriptAndStyle": false,
    "singleQuote": true,
    "semi": false,
    "arrowParens": "avoid"
  },
  "lint-staged": {
    "src/**/*.{js,jsx,vue,json,css,scss,md,html}": [
      "npm run lint"
    ]
  },
  "husky": {
    "hooks": {
      "pre-commit": "",
      "commit-msg": ""
    }
  },
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
}

// env.js

const env = process.env.NODE_ENV
const config = {
  development: {
    NODE_ENV: 'development',
    API: 'www.1111.com',
    PORT: '9527',
    HTTP: 'http://0.0.0.0:3000',
  },
  production: {
    NODE_ENV: 'production',
    API: 'www.22222.com',
    PORT: '3000',
    HTTP: 'http://0.0.0.0:4000',
  },
}
module.exports = config[env]

// commitlint.config.js

module.exports = {
  extends: ['@commitlint/config-conventional'],
  /**
   * git commit -m <type>(<scope>?): <subject>
   * name:[0, 'always', 72]
   * 数组中第一位为level,可选0, 1, 20为disable,1为warning,2error
   * 第二位为应用与否,可选always|never
   * 第三位该rule的值
   */
  rules: {
    'type-enum': [
      2,
      'always',
      ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore'],
    ],
    'type-case': [0],
    'type-empty': [0],
    'scope-empty': [0],
    'scope-case': [0],
    'subject-full-stop': [0, 'never'],
    'subject-case': [0, 'never'],
    'header-max-length': [0, 'always', 72],
  },
}

// .eslintrc.js

module.exports = {
  /**
   * 指定配置文件根目录
   */
  root: true,
  /**
   * eslint解析器配置项
   */
  parserOptions: {
    parser: 'babel-eslint',
    sourceType: 'module', // 指定js的导入方式,设置为 script (默认) 或 module
    jsx: true, // 是否启用JSX
  },
  /**
   * 运行环境极其局全局变量
   */
  env: {
    browser: true,
    es6: true,
    node: true,
  },
  /**
   * 通过extends去引入编码规范。
   * eslint 开头的:是 ESLint 官方的扩展,plugin 开头的:是插件类型扩展,比如 plugin:vue/essential;
   */
  extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
  /**
   * https://www.jianshu.com/p/08bfa54aad8e
   * https://cn.eslint.org/docs/rules/
   * 'off'或0:不启用该规则。'warn'或1:违反时警告。'error'或2:违反时报错。
   * https://guide.aotu.io/docs/css/sass.html
   * 下面的配置是按照京东的规范来配置的
   */
  rules: {
    'prettier/prettier': 'error', // 会将标记地方进行 error 级别的报错提示,然后可以通过 ESLint 的 --fix 自动修复功能将其修复。
    quotes: [
      'error',
      'single',
      {
        // 如果不是单引号,则报错
        avoidEscape: true, // 允许字符串使用单引号或双引号,只要字符串中包含了一个其它引号,否则需要转义
        allowTemplateLiterals: true, // 允许字符串使用反勾号
      },