【Webpack4】CSS 配置之 MiniCssExtractPlugin

10,847 阅读5分钟

Github 仓库地址:github.com/Evelynzzz/r…

版本:Webpack 4.39.1

相关依赖:

判断是开发模式还是生产模式

在配置 Webpack 时,需要区分用于开发模式还是生产模式。比如我们只需要在生产模式时压缩 CSS;而在开发模式的时候,我们又希望生成 Sourcemap 便于调试,以及样式热更新。那么,怎么在 webpack.config.js 中判断开发、生产模式呢?

我通常会定义三个 webpack 配置文件:

  • webpack.config.base.js:通用的配置,比如入口,出口,插件,loader等。以下两个配置文件会引入此配置,再修改添加其他配置。
  • webapck.config.dev.js:开发模式下,启动 webpack-dev-server。
  • webapck.config.prod.js:生产模式下,编译打包。

然后在 package.json 中分别配置了 startbuild 脚本:

{
    "scripts": {
        "start": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --open",
        "build": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --progress --colors -p"
    }
}

注意命令中通过 定义了变量NODE_ENV ,因此在webpack.config.base.js 中可以通过 process.env.NODE_ENV 获取它的值,从而判断时生产模式还是开发模式。

const devMode = process.env.NODE_ENV === 'development'; // 是否是开发模式

接下来进入正题。

提取 CSS 到单独的文件中

在 Webpack 4 之前,我们使用 extract-text-webpack-plugin 插件来提取项目中引入的样式文件,打包到一个单独的文件中。从 Webpack 4 开始,这个插件就过时了,需要使用 MiniCssExtractPlugin

This plugin extracts CSS into separate files. It creates a CSS file per JS file which contains CSS. It supports On-Demand-Loading of CSS and SourceMaps.

此插件为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件,并支持 CSS 和 SourceMap 的按需加载。

注意:这里说的每个包含 CSS 的 JS 文件,并不是说组件对应的 JS 文件,而是打包之后的 JS 文件!接下来会详细说明。

情景一

先举一个基础配置的例子。 webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css'
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader, 'css-loader','postcss-loader' // postcss-loader 可选
        ],
      },{
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader, 'css-loader','postcss-loader','less-loader' // postcss-loader 可选
        ],
      }
    ],
  },
};

基于以上配置,如果入口 app.js 中引用了 Root,Root 引入了 Topics。而 Root.js 中引用样式 main.css,Topics.js 中引用了 topics.css。

// 入口文件 app.js
import Root from './components/Root'

// Root.js
import '../styles/main.less'
import Topics from './Topics'

// Topics.js
import "../styles/topics.less"

这种情况下,Topics 会和 Root 同属一个 chunk,所以会一起都打包到 app.js 中, 结果就是 main.less 和 topics.less 会被提取到一个文件中:app.css。而不是生成两个 css 文件。

            Asset       Size  Chunks                    Chunk Names
          app.css  332 bytes       1  [emitted]         app
           app.js    283 KiB       1  [emitted]  [big]  app

情景二

但是,如果 Root.js 中并没有直接引入 Topics 组件,而是配置了代码分割 ,比如模块的动态引入,那么结果就不一样了:

            Asset       Size  Chunks                    Chunk Names
          app.css  260 bytes       1  [emitted]         app
           app.js    281 KiB       1  [emitted]  [big]  app
 topics.bundle.js   2.55 KiB       4  [emitted]         topics
       topics.css   72 bytes       4  [emitted]         topics

因为这个时候有两个 chunk,对应了两个 JS 文件,所以会提取这两个 JS 文件中的 CSS 生成对应的文件。这才是“为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件”的真正含义。

情景三

但是,如果分割了 chunk,还是只希望只生成一个 CSS 文件怎么办呢?也是可以做到的。但需要借助 Webpack 的配置 optimization.splitChunks.cacheGroups

optimization.splitChunks 是干什么的呢?在 Webpack 4 以前,我们使用 CommonsChunkPlugin 来提取重复引入的第三方依赖,比如把 React 和 Jquery 单独提取到一个文件中。而从 Webpack 4 开始,CommonsChunkPluginoptimization.splitChunks 替代了。从命名也能看出来,它是用来拆分 chunk 的。怎么在这里需要用到这个配置呢?先来看看配置怎么写的:

optimization: {
  splitChunks: {
    cacheGroups: {
      // Extracting all CSS/less in a single file
      styles: {
      	name: 'styles',
        test: /\.(c|le)ss$/,
        chunks: 'all',
        enforce: true,
      },
    }
  }
},

打包结果:

            Asset       Size  Chunks                    Chunk Names
           app.js    281 KiB       2  [emitted]  [big]  app
 styles.bundle.js  402 bytes       0  [emitted]         styles
       styles.css  332 bytes       0  [emitted]         styles
 topics.bundle.js   2.38 KiB       5  [emitted]         topics

可以看出,样式确实都被提取到一个 styles.css 文件中了。但与此同时多了一个 style.bundle.js 文件,这就是 optimization.splitChunks.cacheGroups 的效果。具体原理就不在此深究,感兴趣的话可以研究一下。

MiniCssExtractPlugin vs. style-loader

首先这两个插件用途完全不同:MiniCssExtractPlugin 提取 JS 中引入的 CSS 打包到单独文件中,然后通过标签 <link>添加到头部;style-loader 则是通过 <style> 标签直接将 CSS 插入到 DOM 中。

通常,基本的 CSS 配置都是类似这样的。先 style-loader,然后 css-loader。

module: {
    rules: [
    {
        test: /\.css$/,
            use: [
                'style-loader', 'css-loader'
            ],
        },
    ],
}

但后来由于想要提取 CSS 到单独的文件里,就需要用上 MiniCssExtractPlugin。那么问题来了,如下的配置可行吗?

{
    test: /\.css$/,
        use: [
            'style-loader', MiniCssExtractPlugin.loader, 'css-loader','postcss-loader'
        ],
}

生产模式

根据 MiniCssExtractPlugin 文档 中说到的,此插件适用于没有style-loader 的生产模式中,以及需要 HMR 的开发模式。

This plugin should be used only on production builds without style-loader in the loaders chain, especially if you want to have HMR in development.

也就是说,在生产模式中,以上的配置同时使用了style-loader 和 MiniCssExtractPlugin 是不合适的(试了一下,style-loader不会起作用)。

我们只能取其一。也可以如下两者结合,开发模式中使用 style-loader,生产模式中使用 MiniCssExtractPlugin。各取所需,毕竟这两者的作用还是很不同。

{
	test: /\.css$/,
	use: [
		devMode?'style-loader':MiniCssExtractPlugin.loader,'css-loader','postcss-loader'
	]
}

样式文件热更新(HMR)

从上面引用的那句话也可以看出,在开发模式中, 我们可以用 MiniCssExtractPlugin 实现样式的 HMR(Hot Module Replacement,模块热更新)。

样式文件的 HMR 是指什么呢?如果没有配置 HMR,开发模式下,修改 CSS 源文件的时候,页面并不会自动刷新加载修改后的样式。需要手动刷新页面,才会加载变化。而 HMR 实现了被修改模块的热更新,使得变化即时显示在页面上,不再需要刷新整个页面。

但其实 style-loader也实现了 HMR 接口,如 Wepack 文档的 In a Module 中说到的:

HMR is an opt-in feature that only affects modules containing HMR code. One example would be patching styling through the style-loader. In order for patching to work, the style-loader implements the HMR interface; when it receives an update through HMR, it replaces the old styles with the new ones.

因此开发环境下,这两个插件都是可以热更新 CSS 的,只是 MiniCssExtractPlugin 的配置可能更丰富一些。比如说:style-loader 只热更新 JS 中引入的样式,如果 index.html 中通过 <link> 引入了服务器中的一个CSS 文件:

<link rel="stylesheet" href="/vendors/test.css">
<!-- 通过配置 copy-webpack-plugin 在打包时把 html/vendors/test.css 拷贝到服务器根目录中,因此可以这么链接 -->

如果开发模式下,修改 test.css 的源码,style-loader 不会热更新变化 CSS,而是需要刷新整个页面,但 MiniCssExtractPlugin 则会自动重新加载所有的样式。可能还有其他区别,在此不详细说明了。

MiniCssExtractPlugin 插件可以这么配置 Less 文件的 HMR:

const devMode = process.env.NODE_ENV === 'development'; // 是否是开发模式
//......
module.exports = {
    //......
    module: {
      rules:[
        {
          test: /\.less$/i,
          use:  [
            {
              loader: MiniCssExtractPlugin.loader,
              options: {
                // 只在开发模式中启用热更新
                hmr: devMode,
                // 如果模块热更新不起作用,重新加载全部样式
                reloadAll: true,
              },
            },
            'css-loader','postcss-loader','less-loader'
          ]
        },
        // ......
      ]
    }
}

参考阅读