灏天阁

Webpack5.0从零开始搭建Vue开发环境

· Yin灏

一、前言

平时大家在开发 Vue 项目的时候,大部分应该都是使用 Vue CLI 脚手架生成的项目结构。本篇文章将会使用 Webpack 5.0 去进行 Vue 开发环境的配置,看看平时 Vue CLI 都替我们配置了些什么功能。

准备

如果你阅读过我之前分享关于 Webpack 4.0 的相关文章,那么建议你主要把官网迁移 > 从 v4 升级到 v5 以及 Webpack 5.0 的 概念 这两块的内容给过一遍,有个基本的了解,再对看到感兴趣或疑惑的地方去文档内进行详细阅读。

还需要阅读 Vue 官网 Vue2 文档内生态系统中 Vue Loader 的相关介绍。

二、环境配置

初始化项目

npm init -y

修改 package.json:

package.json

{
  "name": "vue-webpack5-template",
  "version": "1.0.0",
  "description": "",
  "author": "phao",
  "license": "MIT",
  "scripts": {}
}

项目根路径下新建 src 目录,并在 src 内新建 main.js 文件以及 assets 目录。

assets 目录内新建 fonts 以及 img 目录。

安装 Webpack

npm i webpack webpack-cli -D

区分开发与生产环境打包

项目根路径下新建 build 目录,并在 build 内新建 webpack.common.js、webpack.dev.js、webpack.prod.js 进行公共环境、开发环境以及生产环境的 webpack 配置。

修改 package.json 的 scripts 参数,在运行脚本时传入环境变量供脚本内判断使用。

package.json

{
  ...
  "scripts": {
    "build:dev": "webpack --progress --config ./build/webpack.dev.js",
    "build": "webpack --progress --node-env production --config ./build/webpack.prod.js"
  },
  ...
}
npm i webpack-merge -D

webpack.common.js

const path = require('path')

module.exports = {
  entry: {
    main: path.resolve(__dirname, '../src/main.js')
  },
  output: {
    path: path.resolve(__dirname, '../dist')
  }
}

webpack.dev.js

const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common')
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'

const devConfig = {
  mode,
  output: {
    filename: 'js/[name].js',
    chunkFilename: 'js/[name].chunk.js'
  }
}

module.exports = merge(commonConfig, devConfig)

webpack.prod.js

const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common')
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'

const prodConfig = {
  mode,
  output: {
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js'
  }
}

module.exports = merge(commonConfig, prodConfig)

配置 resolve 参数省略部分文件路径编写

webpack.common.js

...

module.exports = {
  entry: {
    ...
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, '../src'),
      '@img': path.resolve(__dirname, '../src/assets/img')
    },
    extensions: ['.js', '.vue']
  },
  ...
}

生成 dist 目录下 index.html

项目根路径下新建 public 目录,并在 public 内新建 index.html。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
  <div id="app"></div>
</body>
</html>
npm i html-webpack-plugin -D

webpack.common.js

...
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'),
      title: 'This is a template'
    })
  ],
  output: {
    ...
  }
}

配置 DevServer 热更新

npm i webpack-dev-server -D

webpack.dev.js

const path = require('path')
...

const devConfig = {
  ...
  devServer: {
    static: path.resolve(__dirname, '../dist'),
    port: 3000,
    open: true,
    hot: true
  },
  output: {
    ...
  }
}

...

package.json

{
  ...
  "scripts": {
    "serve": "webpack-dev-server --progress --config ./build/webpack.dev.js",
    ...
  },
  ...
}

配置 Babel 进行语法转换

npm i babel-loader @babel/core @babel/preset-env -D
npm i @babel/polyfill core-js

webpack.common.js

...

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      }
    ]
  },
  plugins: [
    ...
  ],
  ...
}

项目根路径下新建 babel.config.js。

babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: 3
      }
    ]
  ]
}

对 CSS 以及 Stylus 相关样式的打包

npm i css-loader style-loader postcss-loader autoprefixer stylus stylus-loader -D

webpack.dev.js

...

const devConfig = {
  ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      },
      {
        test: /\.styl(us)$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2
            }
          },
          'postcss-loader',
          'stylus-loader'
        ]
      }
    ]
  },
  output: {
    ...
  }
}

...

项目根路径下新建 postcss.config.js。

postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

项目根路径下新建 .browserslistrc。

.browserslistrc

> 1%
last 2 versions
not dead
not ie 11

生产环境需要抽离成单独的 css 样式文件并且压缩样式代码,开发环境不需要。

npm i mini-css-extract-plugin css-minimizer-webpack-plugin -D

webpack.prod.js

...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
...

const prodConfig = {
  ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      },
      {
        test: /\.styl(us)$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2
            }
          },
          'postcss-loader',
          'stylus-loader'
        ]
      }
    ]
  },
  optimization: {
    minimizer: [
      new CssMinimizerPlugin()
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css'
    })
  ],
  output: {
    ...
  }
}

...

对字体、图片、媒体等静态资源的打包

webpack.common.js

...
const isProduction = process.env.NODE_ENV === 'production'

module.exports = {
  ...
  module: {
    rules: [
      ...
      {
        test: /\.(ttf|woff|woff2|eto|svg)$/,
        exclude: path.resolve(__dirname, '../src/assets/img'),
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 4kb
          }
        },
        generator: {
          filename: isProduction
            ? 'static/fonts/[name].[contenthash:8][ext]'
            : 'static/fonts/[name][ext]'
        }
      },
      {
        test: /\.(jpe?g|png|gif|svg)$/,
        exclude: path.resolve(__dirname, '../src/assets/fonts'),
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024
          }
        },
        generator: {
          filename: isProduction ? 
          'static/img/[name].[contenthash:8][ext]' :
          'static/img/[name][ext]'
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/,
        type: 'asset/resource',
        generator: {
          filename: isProduction ? 
          'static/video/[name].[contenthash:8][ext]' :
          'static/video/[name][ext]'
        }
      }
    ]
  },
  ...
}

对 Vue 单文件组件的打包

npm i vue
npm i vue-loader @vue/compiler-sfc -D

webpack.common.js

...
const { VueLoaderPlugin } = require('vue-loader')
...

module.exports = {
  ...
  module: {
    rules: [
      ...
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      ...
    ]
  },
  plugins: [
    ...
    new VueLoaderPlugin()
  ],
  ...
}

在 src 目录内新建 App.vue。

App.vue

<template>
  <div class="app">{{ msg }}</div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      msg: 'Hello world'
    }
  }
}
</script>

<style lang="stylus" scoped>
.app
  color: skyblue
</style>

编写 src 目录内 main.js。

main.js

import { createApp } from 'vue'
import App from './App'

const app = createApp(App)

app.mount('#app')

集成 Vue Router 及 Vuex

npm i vue-router vuex

在 src 目录内新建 router 以及 store 目录,在 router 内新建 index.js,在 store 内新建 index.js。

编写 router 目录内 index.js。

router > index.js

import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '@/views/About')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

路由模式选择了 history 模式,开发环境需要配置下 DevServer。

webpack.dev.js

...

const devConfig = {
  ...
  devServer: {
    ...
    historyApiFallback: true
  },
  ...
}

...

编写 store 目录内 index.js。

store > index.js

import { createStore } from 'vuex'

const store = createStore({
  state: {
    count: 1
  },
  actions: {
    add ({ commit }) {
      commit('add')
    }
  },
  mutations: {
    add (state) {
      state.count++
    }
  },
  getters: {
    getCount (state) {
      return state.count
    }
  }
})

export default store

修改 src 目录内 main.js。

main.js

import { createApp } from 'vue'
import App from './App'
import router from './router'
import store from './store'

const app = createApp(App)

app.use(router).use(store).mount('#app')

在 src 目录内新建 views 目录,在 views 内新建 Home.vue 以及 About.vue。

Home.vue

<template>
  <div>
    <h3>Home</h3>
    <h3>count: {{ getCount }}</h3>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'Home',
  data () {
    return {}
  },
  computed: {
    ...mapGetters(['getCount'])
  }
}
</script>

<style lang="stylus" scoped>

</style>

About.vue

<template>
  <div>
    <h3>About</h3>
    <h3 @click="add">count: {{ getCount }}</h3>
  </div>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'

export default {
  name: 'About',
  data () {
    return {}
  },
  computed: {
    ...mapGetters(['getCount'])
  },
  methods: {
    ...mapActions(['add'])
  }
}
</script>

<style lang="stylus" scoped>
h3
  color: blue
</style>

在 src > assets > img 目录内放入一张图片。

20221012_01.jpg

修改 src 目录内 App.vue。

App.vue

<template>
  <div class="app">
    <img src="@img/avatar.jpg">
    <p>{{ msg }}</p>
    <nav>
      <router-link to="/">Home</router-link>
      <router-link to="/about">About</router-link>
    </nav>
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      msg: 'Hello world'
    }
  }
}
</script>

<style lang="stylus" scoped>
.app
  color: skyblue
  img
    width: 100px
    height: 100px
    transform: translate(10px)
</style>

定义环境变量

vue 3.x 项目建议设置两个变量以更好地进行 tree-shaking。

webpack.common.js

...
const webpack = require('webpack')
...

module.exports = {
  ...
  plugins: [
    ...
    new webpack.DefinePlugin({
      __VUE_OPTIONS_API__: true,
      __VUE_PROD_DEVTOOLS__: false
    })
  ],
  ...
}

配置 public 目录拷贝文件

npm i copy-webpack-plugin -D

webpack.common.js

...
const CopyPlugin = require('copy-webpack-plugin')
...

module.exports = {
  ...
  plugins: [
    ...
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, '../public'),
          to: path.resolve(__dirname, '../dist'),
          filter: (resourcePath) => {
            if (resourcePath.includes('/public/index.html')) {
              return false
            }

            return true
          }
        }
      ]
    })
  ],
  ...
}

配置 ESLint 规范代码

npm i eslint eslint-webpack-plugin @babel/eslint-parser -D
npm i eslint-config-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-n -D
npm i eslint-plugin-vue -D
npx eslint --init

20221012_02.jpg

这时候项目根路径下会生成配置文件 .eslintrc.js,进行修改。

.eslintrc.js

module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    'standard'
  ],
  parserOptions: {
    parser: '@babel/eslint-parser'
  },
  plugins: [
    'vue'
  ],
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'vue/multi-word-component-names': 0
  }
}

项目根路径下新建 .editorconfig 以及 .eslintignore。

.editorconfig

[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

.eslintignore

/build/
/dist/

webpack 配置文件也需要进行相应的设置。

webpack.common.js

...
const ESLintPlugin = require('eslint-webpack-plugin')
...

module.exports = {
  ...
  plugins: [
    ...
    new ESLintPlugin({
      extensions: ['js', 'jsx', 'ts', 'tsx', 'vue']
    })
  ],
  ...
}

清除上一次打包构建内容

webpack.common.js

...

module.exports = {
  ...
  output: {
    ...
    clean: true
  }
}

配置 SourceMap

一般只有开发环境需要。

webpack.dev.js

...

const devConfig = {
  mode,
  devtool: 'eval-cheap-module-source-map',
  ...
}

...

打包分析

生产环境的打包才需要进行打包分析。

npm i webpack-bundle-analyzer -D

package.json

{
  ...
  "scripts": {
    ...
    "analyze": "webpack --progress --analyze --node-env production --config ./build/webpack.prod.js"
  },
  ...
}

关闭性能提示

生产环境打包时,当打包生成的文件比较大会有性能提示警告,可以通过相应设置进行关闭。

webpack.prod.js

...

const prodConfig = {
  ...
  performance: false,
  output: {
    ...
  }
}

...

调整及小结

在 src 目录内新建 components 目录,项目编写的组件可以放入其中。

项目根路径下新建 .gitignore,避免不必要的文件提交到 git 仓库。

.gitignore

.DS_Store
node_modules
/dist


# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

现在 package.json 的 scripts 参数中共定义了 4 个脚本命令,每个命令的作用如下:

开发环境启动服务
npm run serve

开发环境打包文件
npm run build:dev

生产环境打包文件
npm run build

生产环境打包分析
npm run analyze

该项目结构为:

20221013_01.jpg

最终效果

npm run serve

home 页面

20221013_02.jpg

about 页面

20221013_03.jpg

在 about 页面点击 count 会进行自增。

20221013_04.jpg

三、总结

经过以上一系列的配置,我们使用 Webpack 5.0 完成了一个基础的 Vue 开发环境配置,不得不说这是一个比较繁琐的过程,而且实现的也并不完善,但通过本次练习能够对常用的一些 webpack 配置做到基本的了解和掌握。

在本篇文章的学习中,大家可能会发现有不少地方的配置和 Webpack 4.0 中有所不同,对这些不同之处感兴趣的小伙伴可以去官网阅读相关文档以及网上进行搜索。

本文参考了知乎上这篇用 Webpack 4.0 实现基础的 Vue2 开发环境的文章,“面试官:自己搭建过vue开发环境吗?”

该项目最后的代码我会上传到 码云(gitee.com/phao97)上,大家在配置中遇到问题可以进行参照。

如果觉得本篇文章对你有帮助,不妨点个赞或者给相关的 Git 仓库一个 Star,你的鼓励是我持续更新的动力!

- Book Lists -