Vue2 项目的 Webpack 配置

5,761 阅读5分钟

Webpack 安装

安装 webpack,目前是 webpack5。

npm install -D webpack webpack-cli 

创建 webpack 配置文件。在项目跟目录下创建文件夹:

- build
    - webpack.base.conf.js
    - webpack.dev.conf.js
    - webpack.prod.conf.js

安装 webpack-merge 合并配置文件参数

npm install -D webpack-merge

安装开发环境服务器

npm install -D webpack-dev-server

在 package.json 配置启动脚本,要在脚本传参,需要兼容不同的平台,还要引入一个插件

npm install -D cross-env
"scripts": {
    "start": "npm run dev",
    "build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.conf.js",
    "dev": "cross-env NODE_ENV=development webpack serve --config ./build/webpack.dev.conf.js"
}

基础配置

  • entry:入口文件,webpack 的功能就是将入口文件引用的文件找到,全部塞到入口文件里,因此默认情况,以及没有异步的情况下,打包出来只有一个js文件。
  • output:输出配置
    • path:打包结果的目录
    • publicPath:资源的公共路径,一般用于配置将项目部署到子目录。在 html 中引用打包结果的资源时,会在路径前面加上 publicPath。
    • filename:打包结果的文件名称
    • chunkFilename:打包结果异步模块的文件名称
  • module.rules:非js文件的规则配置,webpack 默认只能解析 js 文件。loader 就是用来解析各种非js文件,顺序是从后往前。
    • webpack5 用 type = 'asset/source' | 'asset/inline' | 'asset/resource' 来代替原来的 raw-loader,url-loader 和 file-loader。type = asset 相当于'asset/inline' | 'asset/resource'的结合,即 file-loader 的功能。
  • resolve.extensions:文件名扩展,可以省略引用文件的后缀名
  • resolve.alias:路径别名
// webpack.base.conf.js

const path = require('path')

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

module.exports = {
    entry: path.resolve(__dirname, '../src/main.js'),
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: isProd ? './' : '/',
        filename: 'js/[name]_[chunkhash:8].js',
        chunkFilename: 'js/[name]_[chunkhash:8].js'
    },
    module: {
        rules: [
            {
                test: /\.(eot|ttf|otf|woff2?)(\?\S*)?$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: { maxSize: 1024 * 5 }
                },
                generator: {
                    filename: 'font/[name]_[hash:8][ext]'
                }
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: { maxSize: 1024 * 5 }
                },
                generator: {
                    filename: 'images/[name]_[hash:8][ext]'
                }
            }
        ]
    },
    resolve: {
        extensions: ['.js', '.vue', '.scss', '.css', '.json'],
        alias: {
            'src': path.resolve(__dirname, '../src'),
            'views': path.resolve(__dirname, '../src/views'),
            'components': path.resolve(__dirname, '../src/components'),
            'directives': path.resolve(__dirname, '../src/directives'),
            'filters': path.resolve(__dirname, '../src/filters'),
            'images': path.resolve(__dirname, '../src/images'),
            'modules': path.resolve(__dirname, '../src/modules'),
            'style': path.resolve(__dirname, '../src/style'),
            'utils': path.resolve(__dirname, '../src/utils'),
        }
    }
}

html 的配置

通过 html-webpack-plugin 可以自动生成一个模板或者指定一个html作为模板。webpack 会将同步模块注入到这个模板。在生产环境时,还可以压缩这个 html。

npm install -D html-webpack-plugin
// webpack.base.conf.js

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

module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            title: 'xxx系统',
            template: 'index.html',
            ...(isProd ? {
                minify: {
                    removeComments: true,
                    collapseWhitespace: true
                }
            }, {})
        })
    ]
}

babel 配置

babel 用来将 es6+ 的语法转换成低版本的js,使之可以在低版本的浏览器上运行。

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

npm install --save core-js@3
// webpack.base.config.js

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

在根目录新增 .babelrc 文件。以下配置就能实现 polyfill 的按需引入。

// .babelrc

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

另一个实现 polyfill 按需引入的方案是 @babel/plugin-transform-runtime + @babel/runtime + @babel/runtime-corejs3。

npm install --save-dev @babel/plugin-transform-runtime

npm install --save @babel/runtime @babel/runtime-corejs3
// .babelrc

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

vue 的配置

npm install -D vue-loader vue-style-loader

vue-loader 用来处理 .vue 文件。v15后的配置更加简单,原先需要在 vue-loader 的参数中指定 loader 处理 .vue 文件内部的脚本和样式,现在只需要引入 VueLoaderPlugin 这个插件即可,处理这些内容的规则与相应后缀名的文件处理相同。

// webpack.base.conf.js
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
    module: {
        rules: [
            // ...
            {
                test: /\.vue$/,
                include: path.resolve(__dirname, '../src'),
                loader: 'vue-loader'
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ],
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
            // ...
        }
    }
}

同时将配置中使用 style-loader 的地方替换成 vue-style-loader。

开发环境配置

通过 webpack-merge 可以合并基础配置。

  • mode: 指定当前环境,设置为 development 或 production 时都能开启一些默认功能。
  • target: 设置该参数是为了处理 HMR 失效的问题
  • devtool: 让控制台的信息映射到真实代码,因为实际上运行的代码时打包处理过的。多个值可选,看需要的信息的详细程度。
  • devServer:开发服务器配置,相当于 express 和 webpack-dev-middleware 的结合
    • port:服务运行的端口
    • hot:是否使用 HMR
    • compress:是否开启 gzip
    • open:运行时是否自动打开窗口
    • proxy:代理
// webpack.dev.conf.js

const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.conf')

module.exports = merge(baseConfig, {
    mode: 'development',
    target: 'web',
    devtool: 'eval-cheap-module-source-map',
    devServer: {
        port: 9090,
        hot: true,
        compress: true,
        open: true,
        proxy: {
            '/api/': {
                target: 'http://example.com:9090',
                changeOrigin: true
            }
        }
    }
})

eslint 配置

eslint 用于语法检查。eslint-loader 即将被 eslint-webpack-plugin 替代。

npm install -D eslint eslint-loader @babel/eslint-parser eslint-plugin-vue
// webpack.dev.conf

module.exports = {
    module: {
        rules: {
            {
                test: /\.(js|vue)$/,
                loader: 'eslint-loader',
                include: path.resolve(__dirname, '../src'),
            }
        }
    }
}

在项目根目录新增 .eslintrc.js 配置文件。

// .eslintrc.js
module.exports = {
    root: true,
    env: {
        'browser': true
    },
    parserOptions: {
        parser: '@babel/eslint-parser'
    },
    extends: [
        'plugin:vue/essential'
        'eslint:recommended'
    ]
}

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

// .eslintignore

/src/lib

生产环境配置

生成环境需要对代码进行压缩和拆分。

npm install -D clean-webpack-plugin

clean-webpack-plugin 插件可以在构建时清除上一次的内容

// webpack.prod.conf.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
    plugins: [
        new CleanWebpackPlugin()
    ]
}

minimize 开启优化,TerserPlugin 配置 js 压缩的相关参数。splitChunks 对模块进行拆分。下面的拆分方法是将 node_modules 里的模块单独打包成一个文件。

// webpack.prod.conf.js

const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({
                extractComments: false,
                terserOptions: {
                  format:{
                    comments: false
                  },
                  toplevel: true
                }
            }),
            `...`
        ],
        splitChunks: {
            cacheGroups: {
                vendors: {
                    chunks: 'all',
                    name: 'vendors',
                    test: /[\\/]node_modules[\\/]/
                }
            }
        }
    }
}

css & sass 的配置

npm install -D style-loader css-loader postcss-loader autoprefixer sass-loader sass-resources-loader

npm install --save postcss
  • css-loader:将 css 解析成字符串
  • style-loader:将 css 字符串作为内联样式插入页面
  • postcss-loader:css后处理器,配合autoprefixer给属性补充浏览器前缀
  • sass-loader:解析sass
  • sass-resources-loader: 可以把资源注入到每个文件,用来设置全局变量。
// webpack.dev.conf.js

module: {
    rules: [
        {
            test: /\.s[ac]ss$/,
            include: srcPath,
            use: [
                'style-loader',
                'css-loader',
                'postcss-loader',
                'sass-loader',
                {
                    loader: 'sass-resources-loader',
                    options: {
                        resources: [
                            path.resolve(__dirname, '../src/style/variable.scss')
                        ]
                    }
                }
            ]
        },
        {
            test: /\.css$/,
            use: [
                'style-loader',
                'css-loader',
                'postcss-loader'
            ]
        },
    ]
}

在根目录创建 postcss.config.js 文件

// postcss.config.js

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

生产环境的配置会有所不同。生产环境需要将 css 抽离成单独的文件,避免 js 文件体积过大。且需要压缩体积。

npm install -D mini-css-extract-plugin css-minimizer-webpack-plugin
  • mini-css-extract-plugin 将 css 抽离成单独的文件
  • css-minimizer-webpack-plugin 用于压缩 css
// webpack.prod.conf.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.conf')

module.exports = merge(baseConfig, {
    mode: 'production',
    module: {
        rules: [
            {
                test: /\.s[ac]ss$/,
                include: srcPath,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'postcss-loader',
                    'sass-loader',
                    {
                        loader: 'sass-resources-loader',
                        options: {
                            resources: commonScssFile
                        }
                    }
                ]
            },
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            ignoreOrder: true,
            filename: 'css/[name]_[contenthash:8].css',
            chunkFilename: 'css/[name]_[contenthash:8].css'
        })
    ],
    optimization: {
        minimizer: [
            new CssMinimizerPlugin()
        ]
    }
})

analyze

通过 webpack-bundle-analyzer 插件可以分析打包结果模块之间的依赖关系和模块大小。

npm install -D webpack-bundle-analyzer
// package.json

{
    "scripts": {
        "analyze": "cross-env ANALYZE=true npm run build"
    }
}
// webpack.prod.conf.js

const isAnalyze = process.env.ANALYZE

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
    plugins: [
        ...(isAnalyze ? [new BundleAnalyzerPlugin()] : [])
    ]
}

完整代码

  • package.json
{
    "scripts": {
        "start": "npm run dev",
        "build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.conf.js",
        "dev": "cross-env NODE_ENV=development webpack serve --config ./build/webpack.dev.conf.js",
        "analyze": "cross-env ANALYZE=true npm run build"
    }
}
  • webpack.base.conf.js
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

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

module.exports = {
    entry: path.resolve(__dirname, '../src/main.js'),
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: isProd ? './' : '/',
        filename: 'js/[name]_[chunkhash:8].js',
        chunkFilename: 'js/[name]_[chunkhash:8].js'
    },
    module: {
        rules: [
            {
                test: /\.(eot|ttf|otf|woff2?)(\?\S*)?$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: { maxSize: 1024 * 5 }
                },
                generator: {
                    filename: 'font/[name]_[hash:8][ext]'
                }
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: { maxSize: 1024 * 5 }
                },
                generator: {
                    filename: 'images/[name]_[hash:8][ext]'
                }
            },
            {
                test: /\.s[ac]ss$/,
                include: srcPath,
                use: [
                    isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
                    'css-loader',
                    'postcss-loader',
                    'sass-loader',
                    {
                        loader: 'sass-resources-loader',
                        options: {
                            resources: path.resolve(__dirname, '../src/style/variable.scss')
                        }
                    }
                ]
            },
            {
                test: /\.css$/,
                use: [
                    isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
                    'css-loader',
                    'postcss-loader'
                ]
            }
            {
                test: /\.js$/,
                use: ['babel-loader?cacheDirectory=true'],
                include: path.resolve(__dirname, '../src'),
                exclude: /node_modules/
            },
            {
                test: /\.vue$/,
                include: path.resolve(__dirname, '../src'),
                loader: 'vue-loader'
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
        new HtmlWebpackPlugin({
            title: 'xxx系统',
            template: 'index.html',
            ...(isProd ? {
                minify: {
                    removeComments: true,
                    collapseWhitespace: true
                }
            }, {})
        })
    ],
    resolve: {
        extensions: ['.js', '.vue', '.scss', '.css', '.json'],
        alias: {
            'src': path.resolve(__dirname, '../src'),
            'views': path.resolve(__dirname, '../src/views'),
            'components': path.resolve(__dirname, '../src/components'),
            'directives': path.resolve(__dirname, '../src/directives'),
            'filters': path.resolve(__dirname, '../src/filters'),
            'images': path.resolve(__dirname, '../src/images'),
            'modules': path.resolve(__dirname, '../src/modules'),
            'style': path.resolve(__dirname, '../src/style'),
            'utils': path.resolve(__dirname, '../src/utils'),
            'vue$': 'vue/dist/vue.esm.js'
        }
    }
}
  • webpack.dev.conf.js
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.conf')

module.exports = merge(baseConfig, {
    mode: 'development',
    target: 'web',
    devtool: 'eval-cheap-module-source-map',
    devServer: {
        port: 9090,
        hot: true,
        compress: true,
        open: true,
        proxy: {
            '/api/': {
                target: 'http://example.com:9090',
                changeOrigin: true
            }
        }
    }
})
  • webpack.prod.conf.js
const isAnalyze = process.env.ANALYZE

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.conf')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = merge(baseConfig, {
    mode: 'production',
    plugins: [
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
            ignoreOrder: true,
            filename: `${assets}/css/vendor.css?v=[contenthash:${hashLen}]`,
            chunkFilename: `${assets}/css/[name].css?v=[contenthash:${hashLen}]`
        }),
        ...(isAnalyze ? [new BundleAnalyzerPlugin()] : [])
    ],
    cache: {
        type: 'filesystem',
        buildDependencies: {
            config: [__filename]
        }
    },
    optimization: {
        minimize: true,
        minimizer: [
            new CssMinimizerPlugin(),
            new TerserPlugin({
                extractComments: false,
                terserOptions: {
                  format:{
                    comments: false
                  },
                  toplevel: true
                }
            }),
            `...`
        ],
        splitChunks: {
            cacheGroups: {
                vendors: {
                    chunks: 'all',
                    name: 'vendors',
                    test: /[\\/]node_modules[\\/]/
                }
            }
        }
    }
})
  • .browserslistrc
> 1%
last 2 versions
ie >= 9
  • .babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
          "useBuiltIns": "usage",
          "corejs": 3
      }
    ]
  ]
}
  • .eslintrc.js
module.exports = {
    root: true,
    env: {
        'browser': true
    },
    parserOptions: {
        parser: '@babel/eslint-parser'
    },
    extends: [
        'plugin:vue/essential'
        'eslint:recommended'
    ]
}
  • .eslintignore
/src/lib
  • postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
};