大家都知道 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:
抽取出 js module 中的 css 字符串. 详见这里 https://github.com/webpack-contrib/mini-css-extract-plugin/blob/v0.4.1/src/loader.js#L113
然后将 css 字符串转为 CssDependency. 详见这里 https://github.com/webpack-contrib/mini-css-extract-plugin/blob/v0.4.1/src/loader.js#L128 和 这里 https://github.com/webpack-contrib/mini-css-extract-plugin/blob/v0.4.1/src/index.js#L136
清空 js module 里的 css 字符串. 详见这里 https://github.com/webpack-contrib/mini-css-extract-plugin/blob/v0.4.1/src/loader.js#L132
然后 MiniCssExtractPlugin 处理 CssDependency 并把结果 push 到 manifest 列表里, 详见这里 https://github.com/webpack-contrib/mini-css-extract-plugin/blob/v0.4.1/src/index.js#L167
因此, 如果我们将 MiniCssExtractPlugin 的 filename 设为 [name].css, 将 webpack 的 output.filename 设为 [name].js, 将 css 文件作为 entry, 如:
- {
- entry: {
- 'index.css': './index.css'
- }
- }
通过 mini-css-extract-plugin 抽取后最终会生成两个文件: index.css.css 和 index.css.js.
所以我们接下来要做的就是:
把 index.css.css 改为 index.css.
把 index.css.js 删掉.
来写个插件
接下来我们来写个插件处理上面的两个事情.
1. 首先是重命名
通过 MiniCssExtractPlugin 源码 https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/src/index.js#L132 , 我们看到 MiniCssExtractPlugin 是在 thisCompilation hook 中将文件加入到 manifest 中. 通过 webpack 源码 https://github.com/webpack/webpack/blob/v4.16.2/lib/Compiler.js#L503 , 我们看到 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: 详细各种例子参照这里 https://github.com/d-band/dool-examples
来源: https://juejin.im/entry/5b56c41bf265da0fa332f51c