[万字逐步详解]使用 webpack 打包 vue 项目(优化生产环境)

Posted GoldenaArcher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[万字逐步详解]使用 webpack 打包 vue 项目(优化生产环境)相关的知识,希望对你有一定的参考价值。

之前在 [万字逐步详解]使用 webpack 打包 vue 项目(基础生产环境) 中比较详尽的手把手带着过了一遍 production 环境的部署,以及在 [万字逐步详解]使用 webpack-dev-server + ESLint 配置 vue 项目的开发环境 中过了一遍使用 webpack-dev-server 配置开发环境,以及使用 ESLint 去提高代码质量的过程。

webpack 配置最后一节就讲一下怎么提高生产环境中打包出来代码的质量。这里会从两个点进行配置的优化:

  • 环境的分离

    原本只有一个 webpack.config.js,但是通过配置开发环境和生产环境,已经很明显的感受到二者的差异,所以有必要对不同的环境进行分离。

  • 功能的优化

    这些包括对 tree shaking 的优化之类的——虽然这些优化在生产环境下默认开启。以及不默认开启的代码分割、魔法注释、CSS 文件的处理,和对文件添加 hash 值。

分离环境

是时候将 webpack 的 生产环境开发环境 分离开来了。从运行结果来说,原本打包的代码可能只有 100KB+,但是配上 Source Map 之后已经到了 1M 多。并且,生产环境并不需要 dev server,也不需要 Source Map,这些冗余的代码无异于会让上线的代码变得更“重”,从而影响访问的效率。

注*:还有一个选择是使用环境变量去判断,然后在同一个文件内对配置进行修改,这个做法也很简单,如:

module.exports = (env, argv) => {
  const config = {
    // ...省略众多基础
  };
  if (env === 'production') {
    config.mode = 'production';
    config.devtool = false;
    config.plugins = [
      // 展开操作符
      ...config.plugins,
      // 新的 plugins
    ];
  }

  return config;
};

这里选择的方式是通过分割不同的配置文件,去对不同的环境进行适配。这种情况下,会有一个开发配置,生产配置,和公共配置,三个配置文件。

环境分析

结合上上一篇的 production build 和上一篇的 开发环境配置的过程中,已经发现了两个环境的差异越来越大了。

其原因是除了编译是开发和生产环境都需要的功能之外,其他的功能开发与生产之间的需求差异挺大的——生产环境下需要尽可能减少代码的尺寸,会额外增添一些 loaders 和 plugins 去减少打包尺寸。而开发环境下需要多次编译,需求是尽可能少的对文件进行处理,提高编译速度。

另外,生产环境下会使用 webpack-dev-server 去生成一个本地服务器,同时为了减少编译速度而开启模块热更新,为了方便 debug 而开启 source map 功能。而生产环境下肯定会开启服务器去 host 网站,所以对 webpack-dev-server 没有什么需求,同样也不会需要 source map 和热更新功能。

简单的列举一个对比,去更直观的了解一下两个环境的差异:

功能开发生产
webpack & webpack-cli
vue-loader & vue-template-compiler
style-loader & less & less-loader
file-loader & url-loader
htmlWebpackPlugin
clean-webpack-plugin×
copy-webpack-plugin×
webpack-dev-server×
Source Map×
HRM×
ESLint×
tree shaking×
code splitting/代码分割×
下文列举的其他针对开发环境优化×

接下来就会将双方都共用的功能,提取到共用配置(common)中,再根据需求去配置开发配置(dev),和生产配置(production)。

共用配置

首先,上面的表格所列举 loaders,也就是开发环境和生产环境都打勾的,是两个环境都需要的,毕竟需要依赖这些 loaders 去解析文件。同理,vue-loader-plugin 和 html-webpack-plugin,这两个也是必须同时作用于两个环境上。

再有,target 和 entry 也是一致的,毕竟这是一个基于 Vue 的 SPA。

至于 output,这个就看项目了,有些项目会分别在 production 和 development 指定不同的文件夹去进行编译,但是这里为了方便起见,用同一个 output 问题也不大。开发环境下使用的 webpack-dev-server 会将文件写入缓存中,不会实际从 output 中提取资源,这也是为什么可以偷懒的原因。

所以 webpack.common.js 的配置就是下面这样的:

const path = require('path');
const webpack = require('webpack');

const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  target: 'web',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      // 解析Vue文件
      { test: /\\.vue$/, loader: 'vue-loader' },
      // 它会应用到普通的 `.js` 文件
      // 以及 `.vue` 文件中的 `<script>` 块
      {
        test: /\\.js$/,
        loader: 'babel-loader',
      },
      // 它会应用到普通的 `.css` 文件
      // 以及 `.vue` 文件中的 `<style>` 块
      {
        test: /\\.css$/,
        use: ['vue-style-loader', 'css-loader'],
      },
      // 对 less 文件进行处理
      {
        test: /\\.less$/i,
        use: [
          // compiles Less to CSS
          'style-loader',
          'css-loader',
          'less-loader',
        ],
      },
      // url-loader,针对图片的优化
      {
        test: /\\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              // 这个上限是官方文档设立的,这里就不改了
              limit: 8192,
              // 同理,不设置为false打开的会是 esModule
              esModule: false,
            },
          },
        ],
      },
    ],
  },
  plugins: [
    // 设置 webpack 所要的基础配置
    new webpack.DefinePlugin({
      BASE_URL: JSON.stringify('./'),
    }),
    // 请确保引入这个插件!
    new VueLoaderPlugin(),
    // 空的狗欧早寒素仅会生成一个 index.html,会引入合适的文件,但是入口DOM节点 app 不见了,所以需要其他的配置
    new HtmlWebpackPlugin({
      template: 'public/index.html',
    }),
  ],
};

生产配置

因为需要覆盖掉 plugins 的一些属性,这里会使用一个名为 webpack-merge 的依赖包去 merge 插件的数组。它的优势在于可以让开发者专注编写对应环境所需要的配置即可,这个插件自己内部会完成不同配置之间的 merge。

  1. 安装 webpack-merge
    注*:使用 webpack-merge 也要注意版本之间的区别,用法可能会有些微不同。

    安装方式依旧是用 npm:

    D:\\vue-webpack>npm i -D
    webpack-merge
    
  2. 新建/修改 webpack.prod.js

    这就是生产模式的代码,主要新增了 mode,以及将原本没有的插件加了回来

    完整源码如下:

    const common = require('./webpack.common');
    const { merge } = require('webpack-merge');
    
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    const CopyPlugin = require('copy-webpack-plugin');
    
    module.exports = merge(common, {
      mode: 'production',
      plugins: [
        new CleanWebpackPlugin(),
        new CopyPlugin({
          patterns: [{ from: 'public/favicon.ico', to: './' }],
        }),
      ],
    });
    
  3. 修改 package.json

    这里只要修改一行代码即可,就是找到 script 下面的那部分,将正确的 webpack 配置文件路放到命令中去:

    {
      // 其它不变
      "scripts": {
        "build": "webpack --config webpack.prod.js"
      }
    }
    
  4. 运行测试没有问题

开发配置

这里主要就是配置一下 devtool 和 devServer 的配置,也是使用 webpack-merge 去进行操作

  1. 新建/修改 webpack.dev.js

    const path = require('path');
    const common = require('./webpack.common');
    const { merge } = require('webpack-merge');
    
    module.exports = merge(common, {
      mode: 'development',
      devtool: 'eval-cheap-module-source-map',
      devServer: {
        open: true,
        // 指定端口
        port: 9000,
        // 命令行中会显示打包的进度
        progress: true,
        // 开启热更新
        hot: true,
        // 是个很有趣的特性
        historyApiFallback: true,
        // 添加静态资源的引用,可以用数组
        // 这样原本没有被打包进去的内容也可以正确被引用
        contentBase: path.join(__dirname, 'public'),
      },
    });
    
  2. 修改 package.json

    方法一样

    {
      // 其它不变
      "scripts": {
        "serve": "webpack serve --config webpack.dev.js"
      }
    }
    
  3. 同样别忘了测试

最后一步就是删除多余的 webpack.config.js,也就是最初建立的配置文件。

功能优化

一些可以提高 webpack 打包性能的配置,具体的链接可以查看 Tree Shaking,目前下面列举的属性在生产环境下都是默认开启的。

webpack 在官网上列举了几个开发环境下配置没什么意义的插件/功能,这里列举一下,将来避免在开发环境使用:

  • TerserPlugin
  • [fullhash]/[chunkhash]/[contenthash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin

不导出死代码

会在 production 模式下自动开启,它不会将未被引用的 dead code 打包近项目中,这个功能又称 tree shaking

在其他环境下,可以通过 optimization 进行配置:

modules.export = {
  // 其他配置不变
  optimization: {
    // 字面意思就是:只 export 使用的代码
    usedExports: true,
  },
};

其他模式下增添代码压缩功能

有需要的话——例如说新增配置与 production 相比更像 development 的 uat 环境之类的——可以使用,实现方法如下:

modules.export = {
  // 其他配置不变
  optimization: {
    // 其他配置不变
    // 最小化
    minimize: true,
  },
};

合并模块

即 scope hoisting 功能,这是一个从 webpack3 开始就退出来的一个特性,可以将几个模块打包到一个文件中去,从而进一步减少文件打包的体积。这个功能在生产环境下默认开启,在其他环境下的开启方法如下:

modules.export = {
  // 其他配置不变
  optimization: {
    // 其他配置不变
    // 合并模块
    // 设置为 false 可以重写 mode: production 中的默认设置
    concatenateModules: true,
  },
};

在 Vue 的项目中,应该会另外实现对于 concatenateModules 功能。一来打包出来的代码其实还是在一个模块里的,二来毕竟这个功能只能用于 ES6 模块,以下是来自 webpack 的说明:

Keep in mind that this plugin will only be applied to ES6 modules processed directly by webpack. When using a transpiler, you’ll need to disable module processing (e.g. the modules option in Babel)

大意为:

注意这个模块只会被应用于被 webpack 直接处理的 ES6 模块。当使用编译器器是,需要禁用模块处理,如:babel 中的 modules 功能

而 Vue 的代码肯定是要通过 babel 被编译的,而 babel 现在应该是默认开启了对 ESModule 的支持。

代码分割

注*:此方法未经过验证。

目的就是为了分包和做到按需加载,有效的减少代码的大小。

这个项目其实没有什么特别好的展示方法,因为只有三个组件,并且存在彼此的依赖关系,不过这里还是会稍微带一下,可以之后再去看。

这里主要的核心概念是在 vue 的 router 组件中使用 webpack 提供的 require.ensure() 去做到按需加载。

require.ensure() 的语法如下:

require.ensure(
  dependencies: String[],
  callback: function(require),
  errorCallback: function(error),
  chunkName: String
)

大概方法如下:

const routes = [
  {
    path: '/',
    name: 'index',
    component: (resolve) =>
      require.ensure([], () => resolve(require('./views'))),
  },
  {
    path: '/otherComp',
    name: 'other compoment',
    component: (resolve) =>
      require.ensure([], () => resolve(require('./views/otherComponent/'))),
  },
  // 差不多的用法
];

注*:除了 webpack 提供的 require.ensure() 之外其实还有其他的方法,不过这里主要还是注重 webpack 相关的学习,所以使用的是 webpack 提供的功能。

魔法注释

注*:此方法未经过验证。

这是让打包后的文件显示文件名的方法,如果文件名相同的将会被打包在一起。

大概方法如下:

const routes = [
  {
    path: '/',
    name: 'index',
    component: (resolve) =>
      require.ensure([], () => resolve(require('./views'), 'index')),
  },
  {
    path: '/otherComp',
    name: 'other compoment',
    component: (resolve) =>
      require.ensure([], () =>
        resolve(require('./views/otherComponent/'), 'otherComp')
      ),
  },
  // 差不多的用法
];

提取 CSS 文件

如果 CSS 的文件体积不是很大,那么直接将 CSS 嵌入到 modules 中说不定运行速度会更快一些——毕竟少了一次请求,还有请求头之类的数据传输。

当然,这里是做功能展示,所以就拆分了——这一步会将 CSS 提取到一个单独的文件中去,然后通过 link 的方式进行引用。

这里依旧会使用插件:mini-css-extract-plugin,去完成这个功能。

注*:这里没有对 .vue 文件内的行内样式进行处理,默认行内样式的尺寸不会特别大——至少不会大到去进行拆分的必要。

  1. 下载插件

    npm install --save-dev mini-css-extract-plugin
    
  2. 使用插件

    这里依旧会选择在生产环境下使用这个插件,具体的配置如下:

    // 新的引用
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    
    module.exports = merge(common, {
      // 新增对CSS的处理,由原本的行内注释改为新增一个CSS文件
      module: {
        rules: [
          {
            test: /\\.less$/i,
            use: [
              // style-loader 不需要了
              // "style-loader",
              MiniCssExtractPlugin.loader,
              'css-loader',
              'less-loader',
            ],
          },
        ],
      },
      plugins: [
        // 新增插件
        new MiniCssExtractPlugin(),
      ],
    });
    
  3. 运行结果

    依旧是使用 npm run build 去执行操作,最后能够发现输出的结果多了一个 css 文件:

    看起来优化到这里可以结束了,但是当我打开 main.css 一看,这才发现,压缩没做:

    回顾一下,webpack 可以直接对 javascript 进行处理,这也就意味着对 CSS 和 HTML 的处理需要依赖其他的 插件(plugins)加载器(loaders)

    换言之,这里需要对 CSS 进行另外的处理。

压缩 CSS 文件

这里其实是有两个选项,分别对应两个版本:

  • webpack v4

    optimize-css-assets-webpack-plugin

  • webpack v5

    css-minimizer-webpack-plugin

这里的 webpack 的版本是 v5,所以会选择用 css-minimizer-webpack-plugin。

  1. 下载插件

    npm install css-minimizer-webpack-plugin --save-dev
    
  2. 使用插件

    这个插件的使用还……有点意思,配错了容易导致其他 optimization 优化失效,具体配置为:

    // 新增引用
    const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
    
    module.exports = merge(common, {
      // 其他配置不变
      optimization: {
        // 注意这个 '...'
        // 它是必须的,并且只能用在 webpack v5 以上
        // 不使用这个 '...' 会导致其他的优化失败
        minimizer: ['...', new CssMinimizerPlugin()],
      },
    });
    
  3. 运行结果

    可以看到,与只使用了 mini-css-extract-plugin 后的结果不一样,使用了 css-minimizer-webpack-plugin 后,css 已经被压缩了:

Hash 输出文件

这是为了客户端缓存进行了优化而使用的功能,一单文件名被 hash 了之后,那么在客户端就可以保存相对而言比较长的时间,而又不用担心更新的问题——新的文件名代表新的请求。

这个配置可以在这里找到:Avoid Production Specific Tooling,总共有三个选项:

  • [fullhash]

    这里的 fullhash 对应的应该就是 v4 的 hash,毕竟看起来结果是一样的:

    fullhash 指的是每次 build 的时候,如果任何内容有所变动,都会重新生成一个。 hash 值,但是所有文件的 hash 是一样的

  • [chunkhash]

    以 chunk 为级别进行重新打包,这种情况下,每个 chunk 的 hash 值都是一样的。这个项目上看不出什么差别,不过如果使用了动态路由,那么每个 router 中引进的 chunk 都是不一样的,这时候就会生成不同的 hash 值。

    同样,只有对应的 chunk 发生了变动,在重新打包的时候,因修改过的 chunk 而生成的文件名就会发生变动。

  • [contenthash]

    这个就是以文件为级别重新进行打包,如果文件产生了变动,那么对应的文件就会生成一个新的 hash 值。

    如下图就能看出来,css 和 js 文件有两个不同的 hash 值:

webpack 官网讲的不是很细致,这里面的解释来自于: Webpack 4: hash and contenthash and chunkhash, when to use which?

修改过的代码在这里:

module.exports = merge(common, {
  // 其余不变
  output: {
    // 其余不变
    filename: '[name]-[contenthash:8].bundle.js',
  },
  plugins: [
    // 其余不变
    new MiniCssExtractPlugin({
      filename: '[name]-[contenthash:8].bundle.css',
    }),
  ],
});

注*contenthash:8:[num] 是用来指定生成既定长度的 hash 值,如 :8 就是 8 位长度。默认好像是 20

这次项目中使用的插件和加载器版本

就是从 package.json 当中拉出来的。

{
  // 是一个 vue2 的项目
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11"
  },
  // 所有使用的开发依赖的版本
  "devDependencies": {
    "@babel/core": "^7.14.6",
    "@vue/cli-plugin-babel": "^4.5.13",
    "babel-eslint": "^10.1.0",
    "babel-loader": "^8.2.2",
    "clean-webpack-plugin": "^4.0.0-alpha.0",
    "copy-webpack-plugin": "^9.0.1",
    "css-loader": "^5.2.6",
    "css-minimizer-webpack-plugin": "^3.0.2",
    "eslint": "^7.30.0",
    "eslint-config-airbnb-base": "^14.2.1",
    "eslint-plugin-import": "^2.23.4",
    "eslint-plugin-vue": "^7.12.1",
    "eslint-webpack-plugin": "^2.5.4",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.3.2",
    "less": "^4.1.1",
    "less-loader": "^10.0.0",
    "mini-css-extract-plugin": "^2.0.0",
    "style-loader": "^3.0.0",
    "url-loader": "^4.1.1",
    "vue-loader": "^15.9.7",
    "vue-style-loader": "^4.1.3",
    "vue-template-compiler": "^2.6.14",
    "webpack": "^5.41.0",
    "webpack-cli": "^4.7.2",
    "webpack-dev-server": "^3.11.2",
    "webpack-merge": "^5.8.0"
  },
  // 这里的内容会被根目录下的 .eslintrc.js 所重写,所以像 extends 和 rules,还是需要参考上文的 .eslintrc.js 中的配置
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": ["plugin:vue/essential", "eslint:recommended"],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  }
}

最后的最后,不知道有没有人会看到这里……有需要项目完整打包上传的吗。

以上是关于[万字逐步详解]使用 webpack 打包 vue 项目(优化生产环境)的主要内容,如果未能解决你的问题,请参考以下文章

[万字逐步详解]使用 webpack-dev-server + ESLint 配置 vue 项目的开发环境

webpack5 查漏补缺

webpack5 查漏补缺

详解Vue webapp项目通过HBulider打包原生APP(vue+webpack+HBulider)

Vue第五天学习笔记之webpack详解

万字总结webpack实战案例配置