【Webpack4】CSS 配置之 MiniCssExtractPlugin

Github 仓库地址:https://github.com/Evelynzzz/react-webpack-boilerplate

版本: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'
          ]
        },
        // ......
      ]
    }
}

参考阅读


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容