从入门到入土的Vue2+webpack

1,829 阅读6分钟

前言:本文只是根据自建的项目所做的文档,文中没有面面俱到,一些小配置并没有在上面写,在实际开发中可能会有写不可预估的出入,希望对各位看官有一定的参考价值

引子:目前社区比较知名的脚手架,有vue-cli、create-react-app,他们都解决了一站式的项目环境配置问题虽然。不过还有一些问题,它们没法解决,比如:1、vue-cli专注的vue项目的环境搭建和配置问题、那每个公司的业务类型不一样,有些公司是金融业务,有些公司是物流业务,如何把相关的东西集成进来?2、每一个公司在项目的沉淀中都是不一样的,比如组件的沉淀、监控埋点方案的沉淀、以及其他样式、目录、service等等一系列编码相关的沉淀,用什么样的方式,能够避免我们的重复劳动? vue-cli 已经非常成熟,成熟到可能自己写的 webpack 性能上不一定比得上vue-cli。当然只是性能上。在实用性,拓展性,可玩性却有很大的操作空间,vue-cli是把饭喂到嘴边,但是并不知道这个饭是如何做的,只见其名不知其意,相比于自己搭建一套环境,你可以知道各个版本的差异化,哪些配置已经废除或者有更好的替代品,不同版本间的环境依赖如何做兼容,哪些方式是最佳实践,对基础业务有更深的理解

关于项目根文件下package.json中的devDependencies--save-d和dependencies--save常见认知:devDependencies用于本地环境开发。dependencies用于生产环境。通过NODE_ENV=developementNODE_ENV=production指定开发或生产环境。更多参考

关于全局变量process.env.NODE_ENV,在webpack5中可以使用new webpack.DefinePlugin({})来定义一个或者多个不同的全局变量(不是windows),里面的值需要JSON.stringify包一层。优先级DefinePlugin > --mode(package.json) > mode(webpack.dev或者webpack.prod中定义的)。更多请戳关于环境变量中的命名规则可以借鉴模式与环境变量 1.初始化:npm init (或者使用默认执行的命令npm init -y) 根目录创建build——webpack.dev.js,webpack.pord.js,webpack.base.js 根目录创建index.html,也可以在public下创建 根目录创建src——App.vue,main.js

2.安装webpack: npm i webpack webpack-cli -D (这里默认是最新版本,如果想使用旧版本的话可以用npm i --save-dev webpack@<version>。不建议全局安装,会将项目中的webpack锁定到指定版本,在使用不同版本的项目中可能会导致构建失败。关于为什么要单独安装webpack-cli——已经从webpack4后版本剥离)

3.安装vue及相关loader: npm i vue@2 -S,npm i vue-loader -D(解析和转换vue文件,交给对应的loader处理),npm i vue-template-compiler -D(编译vue模板的包,传入模板返回AST抽象语法树。vue3替代为@vue/compiler-sfc)

4.样式相关:npm install -D style-loader css-loader postcss-loader autoprefixer sass-loader sass-resources-loader npm install --save postcss npm install less less-loader --save (采用sass与less可供选择。style-loadervue-style-loader之间推荐使用后者)

5.html相关:npm i html-webpack-plugin -D(在打包结束后,自动生成一个html文件,并把打包生成的js文件引入到这个html文件当中)

6.webpack相关配置:npm install webpack-dev-server -D(对代码进行热重载)。npm install -D cross-env(兼容不同平台)。npm i clean-webpack-plugin -D(在打包之前清空output配置的文件夹)

7.文件类配置:npm install file-loader url-loader -D

{
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 默认是 8kb
          },
        },
        generator: {
          filename: 'static/images/[name][hash:5][ext]',
        },
        exclude: /node_modules/,
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        type: 'asset/resource', // 类似 file-loader 导出文件
        generator: {
          filename: 'static/media/[name][hash:5][ext]',
        },
        exclude: /node_modules/,
      },
      // 字体文件
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        type: 'asset/resource',
        generator: {
          filename: 'static/font/[name][hash:5][ext]',
        },
        exclude: /node_modules/,
      }

8.配置文件哈希值:webpack 提供了三种 hash 方式,分别是 hash,chunkhash,contenthash。

hash 是项目工程级的,整个工程构建的文件 hash都是一样的,所以只要工程文件有一个修改了,那么所有打包文件的,hash都会改变,这明显不利于文件缓存,比如第三方库的 chunk
②chunkhash 和hash不一样,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的hash值。我们在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成hash值,那么只要我们不改动公共库的代码,就可以保证其hash值不会受影响
③contenthash 表示由文件内容产生的hash值,内容不同产生的 contenthash值也不一样。在项目中,通常做法是把项目中 css 都抽离出对应的 css 文件来加以引用

配置文件更改:filename:'[name].[contenthash].js'

9.区分运行环境:运行npm run serve后, dist目录被清空了。这并不是我们想看到的,加上还有非常多场景需要我们区分开发和生产环境,比如配置 sourceMap 之类的问题。所以在build文件下再新建webpack.pord.js文件(生产环境),安装npm i webpack-merge -D用来合并配置

// 生产环境
const devConfig = require('./webpack.dev')
const { merge } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = merge(devConfig, {
  mode: 'production',
  plugins: [new CleanWebpackPlugin()]
})

10.针对浏览器配置css前缀自动补全:npm i autoprefixer postcss-loader --save.==注意==:配置 Autoprefixer 之前,需要先添加Browserslist。可以在根目录一个.browserslistrc文件,也可以在package.json 文件中添加:browserslist。不过一定要加,不然 autoprefixer不生效。其次,必须在根目录创建一个 postcss.config.jsimage

package.json:

"browserslist": ["last 1 chrome version","last 1 firefox version","last 1 safari version", "> 1%", "iOS >= 7", "Android > 4.1"]

11.配置CSS样式分离:npm i mini-css-extract-plugin --save. 建议先看这篇文章webpack/vue-cli中的 publicPath 区别 loader改成miniCssExtractPlugin后就不能实现css的热加载 不过开发时也不需要分离css,在生产环境使用即可 webpack.prod.js:

...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
rules:[
{
        test: /\.(less|css)$/,
        use: [{
          loader: MiniCssExtractPlugin.loader,
          options: {
            publicPath: '../' // 注意添加了输出路径
          }
        },{ loader: 'css-loader', options: { esModule: false } },'postcss-loader', 'less-loader']
      },
]
  plugins: [
    new MiniCssExtractPlugin({
    // css的分离到了单独的 css 目录下,并且哈希值继续使用 contenthash
    filename: 'css/[name].[contenthash:4].css'
    })
  ]

12.ES6转ES5:npm i babel-loader @babel/preset-env @babel/core --save npm install --save core-js@3 webpack.dev.js:

rules: [
            {
                test: /\.js$/,
                use: ['babel-loader'],
                include: path.resolve(__dirname, '../src'),
                exclude: /(node_modules)/
            },
        ]

在根目录新增 .babelrc 文件:

{
  "presets": [
    [
      "@babel/preset-env",
      {
          "useBuiltIns": "usage",
          "corejs": 3
      }
    ]
  ]
}

另一个方案是 @babel/plugin-transform-runtime + @babel/runtime + @babel/runtime-corejs3

// .babelrc
{
    "plugins": [
        "@babel/plugin-transform-runtime",
        {
            corejs: 3
        }
    ]
}

13.关于项目热更新:较为复杂的方法为依赖于express平台去使用npm i express webpack-hot-middleware webpack-dev-middleware --save.目前不做详细介绍,坑有些多。有个简单的方法可以满足日常开发需要

plugins:[
    ...
    new webpack.HotModuleReplacementPlugin(),
    // 当开启 HMR 的时候使用该插件会显示模块的相对路径,建议用于开发环境
    new webpack.NoEmitOnErrorsPlugin()
]
devServer: {
    ...
    hot: true,//是否使用 HMR
    open: true,
    compress: true,//是否开启 gzip
    proxy: { //代理
      '/api/': {
          target: 'http://www.baidu.com',
          changeOrigin: true
      }
  }
},

14.生产环境使用splitChunks拆分代码:

optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          priority: 1, // 优先级配置,优先匹配优先级更高的规则,不设置的规则优先级默认为0
          test: /node_modules/, // 匹配对应文件
          chunks: 'initial',
          name: 'vendor',
          minChunks: 1
        },
        commons: {
          priority: 0,
          chunks: 'initial',
          name: 'commons', // 打包后的文件名
          minChunks: 2,
        }
      }
    }
  },
  --------------------------------------------------------------------------
  如果使用了 html-webpack-plugin,需要添加 chunks 配置来自动引入拆分出的 chunk
  new HtmlWebpackPlugin({
    // ...
    chunks: ['moment','main'],
}),
好了,到现在为止的一些配置可以使项目成功的运行和打包了,接下来要创建和安装一些常规的文件夹和开发中必不可少的工具:

1.在src文件夹下新建 router,store,views,utils,styles,components,并安装vuex,vue-router,axios:npm i vuex --save,npm i vue-router --save,npm i axios --save 路由文件要使用按需加载需安装npm install@babel/plugin-syntax-dynamic-import --save babelrc文件中配置:

{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

2.安装eslint:npm install -D eslint eslint-loader @babel/eslint-parser eslint-plugin-vue 在项目根目录新增 .eslintrc.js 配置文件:

module.exports = {
  root: true,
  globals: {
    process: true
  },
  parserOptions: {
    parser: 'babel-eslint',
    sourceType: 'module',
    ecmaFeatures: {
      // 支持装饰器
      legacyDecorators: true
    }
  },
  env: {
    browser: true,
    node: true,
    es6: true
  },

  extends: ['plugin:vue/recommended', 'eslint:recommended'],
  plugins: ['babel', 'prettier'],

  rules: {
    // 使用2个空格缩进
    indent: [
      'error',
      2,
      {
        SwitchCase: 1,
        flatTernaryExpressions: true
      }
    ],
    // switch必须提供 default
    'default-case': 'error',
    // 禁止一成不变的循环,防止代码出现死循环
    'no-unmodified-loop-condition': 'error',
    // 禁止在变量未声明之前使用
    'no-use-before-define': 'error',
    // 代码后不使用分号
    semi: ['error', 'never'],
    // 注释 // 或 /* 之后必须有一个空格
    'spaced-comment': ['error', 'always'],
    // 禁止重复导入模块,对于同一模块内内容,应一次导入
    'no-duplicate-imports': 'error',
    // 必须使用let 或 const, 不能使用var
    'no-var': 'error',
    // 要求大括号内必须有空格
    'object-curly-spacing': ['error', 'always'],
    // 数组前后不需要添加空格
    'array-bracket-spacing': ['error', 'never'],
    // 箭头函数前后必须要有空格
    'arrow-spacing': [
      'error',
      {
        before: true,
        after: true
      }
    ],
    // 代码中可出现console
    'no-console': 'off',
    // 正则中可以出现控制字符
    'no-control-regex': 'off',
    'no-unused-vars': [
      'error',
      {
        ignoreRestSiblings: true,
        // 可以声明未使用的h,方便jsx
        argsIgnorePattern: 'h'
      }
    ],
    // 行注释必须在行上面
    'line-comment-position': ['error', { position: 'above' }],
    // 组件名称必须是大驼峰
    'vue/name-property-casing': ['error', 'PascalCase'],
    // vue Html元素单标签关闭方式
    'vue/html-self-closing': [
      'error',
      {
        html: { normal: 'never', void: 'always' },
        svg: 'always',
        math: 'always'
      }
    ],
    // 组件在template内必须使用 kebab-case 格式
    'vue/component-name-in-template-casing': [
      'error',
      'kebab-case',
      {
        registeredComponentsOnly: false,
        ignores: []
      }
    ],
    // template 内必须使用 ===
    'vue/eqeqeq': 'error',
    // 允许使用v-html
    'vue/no-v-html': 0,
    // 禁用隐式的eval() 比如 setTimeout('alert();', 100)
    'no-implied-eval': 'error'
  },
}

.eslintignore 文件可以忽略那些不用语法检查的文件:

node_modules
/dist

3.安装element-ui按照官网走便是,值得一提的是引入样式文件可能会与dev文件配置的属性有冲突,把exclude: /(node_modules)/删掉即可。另个是关于官网上写的babel转es2015的问题,可以将 "presets": [["es2015", { "modules": false }]]中的es2015换成已经配置的@babel/preset-env。全部引入和按需引入只能存在一个

优化相关
resolve: {
    alias: { //配置别名可以加快搜索速度
      '@': path.resolve(__dirname, '../src'),
    },
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.vue', '.sass', '.less'],//指定 extensions 之后,使用require 和 import 的时候就不需要加文件扩展名了,查找的时候会依次匹配,但同一个目录有不同类型的同名文件时,也只会匹配第一个
    modules: [ //指定第三方依赖的存放目录,默认为'node_modules'
      path.resolve(__dirname, '../node_modules'),
      'node_modules'
  ]
  },
缓存
1. //webpack.dev.js,缓存之前打包的内容,配置之后会生成一个.cash文件夹,通过文件缓存,直接缓存到本机磁盘
cache: {
    type: 'filesystem',   //默认缓存到 node_modules/.cache/webpack。还可以使用 cacheDirectory选项自定义配置
},
2.babel-loader 自带缓存配置。开启后会将缓存放在node_modules/.cache/babel-loader
{
    loader: 'babel-loader',
    options: {
        cacheDirectory: true,
    }
}
3.使用此npm i cache-loader -D,可以将其他 loader 的结果缓存到磁盘中,默认路径node_modules/.cache/cache-loader
{
    test: /\.css$/,
    use: [
        'cache-loader',
        'style-loader',    
        'css-loader'
    ],
},
JS 压缩
npm i terser-webpack-plugin -D

// webpack.prod.js
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin({ // 压缩JS代码
        terserOptions: {
          compress: {
            drop_console: true, // 去除console
          },
        },
      })],
  },
};
HTML 压缩
1.html-webpack-plugin的 minify 选项用于设置html文件的压缩
 minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true,
    },
2.使用 html-minifier-terser,webpack5 开始,像压缩类的插件,应该配置在 optimization.minimizer 数组中,方便统一管理
npm i html-minimizer-webpack-plugin -D

// webpack.prod.js
const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");

optimization: {
    minimize: true,
    minimizer: [
    ....
      new HtmlMinimizerPlugin(),
    ],
}
CSS 压缩
npm i css-minimizer-webpack-plugin -D

// webpack.prod.js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  optimization: {    // 告知 webpack 使用 TerserPlugin 或其它在 optimization.minimizer 定义的插件压缩 bundle。
    minimizer: [
    ...
      new CssMinimizerPlugin(),
    ],
  },
};
构建时间优化`thread-loader`:多进程打包,可以大大提高构建的速度,使用方法是将thread-loader放在比较费时间的loader之前,比如babel-loader

npm i thread-loader -D

{
        test: /\.js$/,
        use: [
          'thread-loader',
          'babel-loader'
        ],
      }
}
exclude & include

exclude:不需要处理的文件
include:需要处理的文件

    {
        test: /\.js$/,
        //使用include来指定编译文件夹
        include: path.resolve(__dirname, '../src'),
        //使用exclude排除指定文件夹
        exclude: /node_modules/,
        use: [
          'babel-loader'
        ]
      },

source-map 产生的文件映射了压缩后的代码所对应的转换前的源代码位置,解决了代码压缩后难以调试的问题

// webpack.dev.js开发环境

module.exports = {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map'
}

// webpack.prod.js生产环境

module.exports = {
  mode: 'production',
  devtool: 'nosources-source-map'
}
压缩打包的文件
npm i compression-webpack-plugin -D
// webpack.prod.js
const CompressionPlugin = require('compression-webpack-plugin')
new CompressionPlugin({
      algorithm: 'gzip',
      threshold: 10240,
      test: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i,
      minRatio: 0.8,
      deleteOriginalAssets: true 
    })

打包体积分析
npm i webpack-bundle-analyzer -D

// webpack.prod.js

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

  plugins: [
    new BundleAnalyzerPlugin(),
]
小图片转base64
webpack5中url-loader已被废弃,改用asset-module
 {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 25 * 1024, // 默认是 8kb
          },
        },
        generator: {
          filename: 'static/images/[name][hash:5][ext]',
        },
        exclude: /node_modules/,
      },
构建进度条
npm i progress-bar-webpack-plugin -D 或者npm i webpackbar -D

// webpack.base.js
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
plugins: [
    ...
    new ProgressBarPlugin() 或 new WebpackBar(),
  ],
自适应布局解决方案
npm install postcss postcss-pxtorem@5.1.1 --save-dev (vue2项目不宜装太高版本)
npm i lib-flexible-computer --save-dev
这里要说明lib-flexible-computer这个依赖,大部分pc的适配都是使用的npm i lib-flexible -S,这个依赖,做适配的话只能做到屏幕540一下的,pc端使用并不是很好用
在util文件夹下创建rem.js
(function flexible (window, document) {
  var docEl = document.documentElement
  var dpr = window.devicePixelRatio || 1

  function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = 12 * dpr + 'px'
    } else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }

  setBodyFontSize()

  function setRemUnit () {
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
  }

  setRemUnit()

  window.addEventListener('resize', setRemUnit)
  window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
  })

  if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines')
    }
    docEl.removeChild(fakeBody)
  }
})(window, document)

main.js中引入:import "lib-flexible-computer";import '@/utils/rem'
postcss.config.js中配置:
module.exports = {
  plugins: {
    'autoprefixer': require('autoprefixer'),
    'postcss-pxtorem': {
      rootValue: 192,
      propList: ['*']
  }
  } 
}
package.json中添加:
"postcss": {
    "plugins": {
      "autoprefixer": {
        "overrideBrowserslist": [
          "Android 4.1",
          "iOS 7.1",
          "Chrome > 31",
          "ff > 31",
          "ie >= 8"
        ]
      },
      "postcss-pxtorem": {
        "rootValue": 192,
        "propList": [
          "*"
        ]
      }
    }
}

==提示==:1.vue2不要安装最新的vue-loader,15即可

2.关于webpack-dev-server中的devServer配置,在5版本中弃用了一些属性,比如说contentBase:""更改为static:{},图中是新属性供参考 image

3.css热更新不生效:①.webpack内解决:css-loader 4.0 后默认对 esModule 设置的是true,而 vue-style-loader4.1.0默认接收的是commonjs的结果,也就是默认接收的是“css-loaderesModule 设置的是false的结果”,导致样式无法加载 image ②.vue-cli内解决:cli.vuejs.org/zh/config/#…

4.关于autoprefixer可能要进行降级处理

5.使用webpack-merge后,两个环境的配置不要相同

6.在vue2中使用vuex与vue-router不要默认安装最新版本,应使用3版本。路由模式history开启需要后端支持,前端独自开启会显示页面不存在

7.如果想使用@导入组件显示路径的话,可以如下配置:打开文件 - 首选项 - 设置 - 搜索 Path Intellisense - 打开 settings.json ,添加:

"path-intellisense.mappings": {
     "@": "${workspaceRoot}/src"
 }
 在vue项目 package.json 所在同级目录下创建文件 jsconfig.json:
 {
  "compilerOptions": {
      "target": "ES6",
      "module": "commonjs",
      "allowSyntheticDefaultImports": true,
      "baseUrl": "./",
      "paths": {
        "@/*": ["src/*"]
      }
  },
  "exclude": [
      "node_modules"
  ]
}

8.sass变量导出undefined问题记录

主要是在css-loader版本较新的情况下才会需要以.module.scss结尾
{
    test: /\.s[ac]ss$/i,
    use: [
        "style-loader",
        {
            loader: "css-loader",
            options: {
                modules: {
                    namedExport: false,
                },
            },
        },
        'sass-loader'
    ]
}