黑魔法之 CSS Entry

大家都知道 webpack 的 Entry 都是 js,如果想输出 css 文件只能在 js 文件里导入 css,所以很多人都会想要是 entry 也可以是 css 那多好,这样就可以任意输出 css 而不需要在 js 文件里引入。那么问题来了:怎么让 webpack 支持 css 作为 entry 呢? PS:以下 webpack 版本 4.16.2,mini-css-extract-plugin 版本 0.4.1

来一打原理

首先我们理一下输出 css 的原理:

1. 通过 css-loader 处理 css 文件。

css-loader 通过 postcss 处理完 css 然后将 css 字符串拼接成 js module。如:

.a { color: red }

转为类似于:

exports = module.exports = require("css-loader/lib/css-base.js")(false);
// module
exports.push([module.id, ".a{color:red}", ""]);

2. 通过 mini-css-extract-plugin 抽取出 css 文件。

首先通过 MiniCssExtractPlugin.loader 处理 css-loader 输出的 js module:

  1. 抽取出 js module 中的 css 字符串。详见这里
  2. 然后将 css 字符串转为 CssDependency。详见这里这里
  3. 清空 js module 里的 css 字符串。详见这里

然后 MiniCssExtractPlugin 处理 CssDependency 并把结果 push 到 manifest 列表里,详见这里

因此,如果我们将 MiniCssExtractPlugin 的 filename 设为 [name].css,将 webpack 的 output.filename 设为 [name].js,将 css 文件作为 entry,如:

{
  entry: {
    'index.css': './index.css'
  }
}

通过 mini-css-extract-plugin 抽取后最终会生成两个文件:index.css.cssindex.css.js

所以我们接下来要做的就是:

  1. index.css.css改为 index.css
  2. index.css.js删掉。

来写个插件

接下来我们来写个插件处理上面的两个事情。

1. 首先是重命名

通过 MiniCssExtractPlugin 源码,我们看到 MiniCssExtractPlugin 是在 thisCompilation hook 中将文件加入到 manifest 中。通过 webpack 源码,我们看到 webpack 先处理 thisCompilation hook,然后处理 compilation hook。因此我们要在 thisCompilation 后的 compilation hook 中进行更名。

2. 然后删除对应的 js 文件

这个比较简单,只需要在 emit 阶段筛选 chunk.files 将对应的 file 从 compilation.assets 里删除就可以了。

3. CSSEntryPlugin

const RE_CSS = /\.css$/i;
const RE_NAME = /\[name\]/gi;
const RE_JS_MAP = /\.js(|\.map)$/i;

class CssEntryPlugin {
  apply (compiler) {
    // 1\. 重命名文件
    compiler.hooks.compilation.tap('CssEntryPlugin', (compilation) => {
      compilation.mainTemplate.hooks.renderManifest.tap('CssEntryPlugin', (result) => {
        // 遍历 result(即 manifest)
        for (const file of result) {
          const { filenameTemplate, pathOptions } = file;
          const { chunk } = pathOptions || {};
          const name = chunk && (chunk.name || chunk.id);
          // 如果 chunk 的 name 以 .css 结尾且 filename 是字符串,则重命名文件
          if (RE_CSS.test(name) && typeof filenameTemplate === 'string') {
            // 将 [name] 替换成不带后缀的文件名
            const rename = name.replace(RE_CSS, '');
            file.filenameTemplate = filenameTemplate.replace(RE_NAME, rename);
          }
        }
        return result;
      });
    });
    // 2\. 删除没用的 js 文件
    compiler.hooks.emit.tapAsync('CssEntryPlugin', (compilation, callback) => {
      compilation.chunks.filter(chunk => {
        // 首先筛选出 css chunk
        return RE_CSS.test(chunk.name);
      }).forEach(chunk => {
        // 删除 .js 或 .js.map 后缀的文件
        chunk.files.forEach(file => {
          if (RE_JS_MAP.test(file)) {
            delete compilation.assets[file];
          }
        });
      });
      callback();
    });
  }
}

一个轮子

想让 webpack 支持 CSS Entry 上面的 CssEntryPlugin 大家可以直接用。另外我们将其封装在了 dool 里面。dool 是一个 webpack 的封装,简化了 webpack 的配置,封装了一些常用的前端打包模式。比如 css entry 配置:

// file: .doolrc
{ files: ['./style/*.less', './js/*.js'] }

等价:

{
  entry: {
    'style/page_a.css': './style/page_a.less',
    'style/page_b.css': './style/page_b.less',
    'style/page_c.css': './style/page_c.less',
    'js/page_a': './js/page_a.js',
    'js/page_b': './js/page_a.js'
  }
}

PS:详细各种例子参照这里

原文链接:zhuanlan.zhihu.com

上一篇:移动端城市定位,城市区域代码adcode
下一篇:leancloud + react-native实时通信问题整理

相关推荐

官方社区

扫码加入 JavaScript 社区