3. Webpack生产环境配置

896 阅读7分钟

1. 前言

📖回顾:
上一篇文章webpack开发环境配置从文件类型的角度出发,简单介绍了样式、html、图片等资源要如何打包,还简单配置了一下开发服务器devServer,没印象的话可以回去复习一下哦~

与上一篇文章不同的是,本文会从文件内容的角度出发,介绍样式、html、js的文件提取兼容性处理压缩。可能不会深入介绍每个配置项,有需要可以挨个去查阅官方文档~

另外,从篇幅考虑,只会列出必要的代码,完整的代码在这里

2. css文件

2.1. 提取css成单独文件

使用的plugin:mini-css-extract-plugin

思路

首先,在上一篇开发环境中,webpack的配置是这样的:

  • css-loader:将css文件转换成commonjs模块加载到js中,css代码被转换成了样式字符串。(转换后得到的commonjs模块可以理解为:用js给元素动态添加样式的那种代码);
  • style-loader:创建style标签,将css-loader生成的样式资源插入进去,添加到head中,使样式生效。
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'built.js',
    path: resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader', // 创建style标签,将'css-loader'整合到js中的样式字符串放到style标签中。
          'css-loader' // 将css文件转换成commonjs模块加载到js中,css代码被转换成了样式字符串。
        ]
      }
    ]
  },
  plugins: [],
  mode: 'development'
}

现在,我们想提取css成一个文件。显然,不再需要style-loader将样式代码塞进<style></style>标签了。需要的是另外一个loader或plugin,用于生成一个单独的文件,并将样式代码塞进去。

webpack配置

const { resolve }  = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 创建style标签,将'css-loader'整合到js中的样式放到style标签中

          // 用这个loader取代style-loader,因为想将css单独输出为一个文件
          MiniCssExtractPlugin.loader, // 作用:提取js中的css成单独文件

          'css-loader' // 将css文件整合到js中
        ]
      }
    ]
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: './src/index.html'
    }),
    new MiniCssExtractPlugin({
      // 对输出的css文件重命名
      filename: 'css/built.css'
    })
  ],
  mode: 'development'
}

2.2. css兼容性处理

使用的loader:postcss-loader

需要下载两个包:postcss-loaderpostcss-preset-env

  • postcss-loader:根据node环境变量生成对应环境的兼容性代码;

    • 设置环境变量:process.env.NODE_ENV = 'development';
    • 如果不手动设置node环境变量,默认为"production"。
  • postcss-preset-env:帮助postcss-loader找到package.json中browserslist的配置,大概长下边这样:

      "browserslist": {
        // 开发环境,这里对应的是node的环境变量(非webpack.config.js配置的mode):process.env.NODE_ENV = development
        "development": [
          "last 1 chrome version", //chrome最新的一个版本
          "last 1 firefox version",
          "last 1 safari version"
        ],
        "production": [
          ">0.2%", // 兼容市面上(使用人数?待查)最靠前的99.8%的浏览器
          "not dead", // 兼容非死亡的浏览器
          "not op_mini all" // 兼容非open mini浏览器
        ]
      }
    

思路

回顾一下2.1中css文件的打包流程:先用 css-loader将样式加载到js中;再用MiniCssExtractPlugin.loader将样式打包进单独的文件。

进行兼容性处理的话,我们想一下,相对简单的处理顺序可能有两种:在css-loader前或MiniCssExtractPlugin.loader后。 这里选择在css-loader前,为什么是这个顺序我现在还没有搞清楚,知道的伙伴可以分享一下🙏🙏

webpack配置

1)先要在package.json中配置字段browserslist

  "name": "webpack-study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "browserslist": {
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ],
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ]
  },
}

2)webpack配置:

const { resolve } = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

// 配置node的环境变量
process.env.NODE_ENV = 'development';

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                ident: 'post-css',
                plugins: [
                  'postcss-preset-env'
                ]
              }
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: './src/index.html'
    }),
    new MiniCssExtractPlugin({
      filename: 'css/built.css'
    })
  ],
  mode: 'development'
}

2.3. 压缩css

使用的plugin:optimize-css-assets-webpack-plugin

webpack配置

const { resolve } = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const MiniCSSExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');

// 配置node的环境变量 - css兼容性处理时使用
// process.env.NODE_ENV = 'development';

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCSSExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                ident: 'postcss',
                plugins: [
                  'postcss-preset-env'
                ]
              }
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: './src/index.html'
    }),
    new MiniCSSExtractPlugin({
      filename: 'css/built.css'
    }),
    // 压缩css
    new OptimizeCSSAssetsWebpackPlugin()
  ],
  mode: 'development'
}

3. JS文件

3.1. JS语法检查eslint

使用的loader:eslint-loader

需要下载:

  • eslint-loadereslint:eslint-loader是基于eslint的(可以类比理解为:js与nodejs环境的关系);
    • 可以在package.json中增加eslintConfig字段,配置eslint。比如可以在eslintConfig中设置检查规则:"eslintConfig": { "extends": "airbnb-base" }
  • eslint-config-airbnb-baseeslint-plugin-import:因本文会设置airbnb-base的检查规则来进行语法检查,而到eslint-config-airbnb-base官网可以查到还需下载eslint-plugin-import

webpack配置

1)先要在package.json中配置字段eslintConfig

{
  "name": "webpack-study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "eslintConfig": {
    "extends": "airbnb-base",
    "env": {
      "browser": true
    }
  }
}

2)webpack配置:

const { resolve } = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'eslint-loader',
        options: {
          // 自动修复eslint的错误
          fix: true
        }
      }
    ],
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: './src/index.html',
    }),
  ],
  mode: 'development',
};

3.2. JS兼容性处理

1)什么是JS兼容性处理?
其实就是把ES6及以上(后边简写为ES6+)新增的语法、API处理成ES5及以下的版本,解决某些浏览器(ie)上的兼容性报错问题。

2)Babel简介:
Babel 是一个工具链,主要用于在当前和旧的浏览器或环境中,将ES6+代码转换为JavaScript向后兼容版本的代码。

3)polyfill是啥?
⁉️:不知道大家之前有没有过这种疑问:只知道babel是处理ES6+兼容的,polyfill分别是干嘛的😵?
🅰️:其实,ES6+语法和新增的API需要分开进行处理,polyfill是针对新增API的~(比如:箭头函数是新增语法,Promise是新增的API)

4)请大家跟我一起看一段代码(下面JS兼容性处理相关内容将以这个作为例子):

// 新语法
const add = (x, y) => {
  return x + y;
};
console.log(add(2, 5));

// 新API
const promise = new Promise(resolve => {
  setTimeout(() => {
    console.log('定时器执行完了~');
    resolve();
  }, 1000);
});

console.log(promise);

上面代码如果不做任何兼容性处理,运行结果如下: ie无法识别ES6+的内容

3.2.1. 处理语法

1)下载
  • 基础核心包:babel-loader@babel/core
  • 处理语法:@babel/preset-env
2)webpack配置
// 以下是单个loader配置
{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  presets: [
    [
      '@babel/preset-env'
    ]
  ]
}
3)运行结果

IE仍然无法识别ES6+新增的API

3.2.2. 处理语法+API(非按需加载)

1)下载
  • 基础核心包:babel-loader@babel/core
  • 处理语法:@babel/preset-env;
  • 处理API(直接在js中引入):@babel/polyfill
2)配置

webpack.config.js:

// 以下是单个loader配置
{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  presets: [
    [
      '@babel/preset-env'
    ]
  ]
}

在上面的例子最顶部加import '@babel/polyfill';

import '@babel/polyfill';

const add = (x, y) => {
  return x + y;
};
console.log(add(2, 5));

const promise = new Promise(resolve => {
  setTimeout(() => {
    console.log('定时器执行完了~');
    resolve();
  }, 1000);
});

console.log(promise);

3)运行结果

4) 缺点

我们来看看打包后的代码:

可以看出,虽然解决了JS兼容性问题,但这种方法将ES6+新增的所有API都打包进来了,造成了代码的冗余。事实上我们只需要箭头函数和Promise的部分==

3.2.3. 处理语法+API(按需加载)

1)下载
  • 基础核心包:babel-loader@babel/core
  • 处理语法和API:@babel/preset-env + 配置;
    • 配置基于:core-js
2)webpack配置

注意:不需要在js中import @babel/polyfill

// 以下是单个loader配置
{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  options: {
    // 预设:指示babel做怎么样的兼容性处理
    presets: [
      [
        '@babel/preset-env',
        {
          /** https://babeljs.io/docs/en/babel-preset-env#usebuiltins
           * useBuiltIns:配置@babel/preset-env如何处理polyfills,取“usage”,“entry”,“false”之一,默认为“false”
           *  - 当使用usage或entry选项时,@babel/preset-env将添加对core-js模块的直接引用,类似import(或require)。这意味着core-js将相对于文件本身进行解析,并且按需引入。
           */
          useBuiltIns: 'usage', // 按需加载
          corejs: { version: 3 }, // 指定corejs的版本
          targets: { chrome: '60', firefox: '60', ie: '9', safari: '10', edge: '17' } // 指定兼容到浏览器哪个版本
        }
      ]
    ]
  }
}
3)运行结果

3.3. 压缩JS

非常简单,只需要把mode设置为“production”。

原因:“production” mode下,webpack会自动启用TerserWebpackPlugin/UglifyJsPlugin来压缩。

4. HTML文件

HTML文件不像CSS和JS,不需要做兼容性处理。

4.1. 压缩HTML

使用的plugin:html-webpack-plugin + 配置

webpack配置

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build')
  },
  plugins: [
    // html压缩只需配置html-webpack-plugin
    new HTMLWebpackPlugin({
      template: './src/index.html',
      minify: {
        removeComments: true, // 移除注释
        collapseWhitespace: true // 移除空格
        // 更多配置可以自行探索😝~
      }
    })
  ],
  mode: 'production'
}

5. 思考和总结

5.1. 捋一捋

🎯有部分资源的打包放在了上一次webpack开发环境配置中。
从文件类型出发,一起来捋捋👉

  • 先是样式(css/less/sass):
    • css文件:
      • postcss-loader处理css兼容性(还需下载postcss-preset-env,在package.json中配置browserslint字段);
      • css-loader将css文件转换成commonjs模块加载到js中;
      • mini-css-extract-plugin将js中的css模块提取成单独的一个文件;
      • optimize-css-assets-webpack-plugin压缩css;
    • less文件:
      • 在css文件的基础上多了less-loader(需在压缩前执行,还需下载less);
    • sass文件:
      • 在css文件的基础上多了sass-loader(需在压缩前执行,还需下载node-sass);
  • 再是js文件:
    • eslint-loader做语法检查(还需下载eslinteslint-config-airbnb-baseeslint-plugin-import eslint,在package.json中配置eslintConfig字段)
    • babel-loader处理js兼容性:
      • babel-loader+@babel/core+@babel/preset-env:只能处理ES6+语法问题;
      • babel-loader+@babel/core+@babel/preset-env+ @babel/polyfill:以非按需加载的方式处理js全部兼容性问题;
      • babel-loader+@babel/core+@babel/preset-env+ 配置:以按需加载的方式处理js全部兼容性问题(推荐);
    • 将mode设置为production,自动压缩js代码;
  • 然后是html文件:
    • html-webpack-plugin创建一个空的html并自动引入所有webpack打包输出的资源;
      • 再对其进一步配置,做到压缩html代码。
  • 还有图片资源:
    • url-loader处理css中的图片资源(img标签除外)(还需下载file-loader);
    • 针对img标签:html-loader处理html文件的img图片,从而能被url-loader处理;
  • 最后其他资源:file-loader

5.2. 总的webpack配置

跟着捋出来的思路,可以写一下~

const { resolve } = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');

process.env.NODE_ENV = 'production';

const commonCssLoader = [
  MiniCssExtractPlugin.loader,
  // 'style-loader', // 直接将css放到style标签里
  'css-loader',
  {
    loader: 'postcss-loader',
    options: {
      postcssOptions: {
        ident: 'post-css',
        plugins: [ 'postcss-preset-env' ] // 还需要在package.json中配置browserslist
      }
    }
  }
];

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [...commonCssLoader]
      },
      {
        test: /\.less$/,
        use: [ ...commonCssLoader, 'less-loader' ]
      },
      /**
       * 注意:正常来讲,一个文件只能由一个loader处理。
       * 当一个文件要被多个loader处理,需要指定loader执行的先后顺序。
       * 这里:应先执行eslint-loader,再执行babel-loader
       */
      {
        // 需要在package.json中配置eslintConfig
        test: /\.js$/,
        exclude: /node_modules/,
        enforce: 'pre', // 优先执行
        loader: 'eslint-loader',
        options: {
          fix: true
        }
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: [
            [
              '@babel/preset-env',
              {
                useBuiltIns: 'usage',
                corejs: { version: 3 },
                targets: {
                  chrome: '60',
                  firefox: '50',
                  ie: '9'
                }
              }
            ]
          ]
        }
      },
      {
        test: /\.(jpg|png|gif)$/,
        loader: 'url-loader',
        options: {
          limit: 8 * 1024,
          name: '[hash:10].[ext]',
          outputPath: 'images',
          esModule: false
        }
      },
      {
        test: /\.html$/,
        loader: 'html-loader',
        options: {
          esModule: false
        }
      },
      {
        exclude: /\.(js|html|css|less|jpg|png|gif)$/,
        loader: 'file-loader',
        options: {
          outputPath: 'assets'
        }
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/built.css'
    }),
    new OptimizeCssAssetsWebpackPlugin(),
    new HTMLWebpackPlugin({
      template: './src/index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true
      }
    })
  ],
  mode: 'production'
}