前言

前面我们写过几篇关于webpack的文章:

  • webpack源码解析一
  • webpack源码解析七(optimization)

然后结合之前babel、eslint知识搭了一个比较复杂的vue项目:

  • webpack实战之(手把手教你从0开始搭建一个vue项目)
  • 手把手教你从0开始搭建一个vue项目(完结)

在实战demo中我们有用到一个css的插件mini-css-extract-plugin,今天我们结合demo来分析一下源码。

简介

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.

It builds on top of a new webpack v4 feature (module types) and requires webpack 4 to work.

Compared to the extract-text-webpack-plugin:

  • Async loading
  • No duplicate compilation (performance)
  • Easier to use
  • Specific to CSS

以上是官网的介绍~

mini-css-extract-plugin主要功能就是:“抽离css到单独的文件”,跟“extract-text-webpack-plugin”插件相比有以下优点:

  • 异步加载
  • 不会重复编译
  • 使用很方便
  • 特定于css

准备

我们还是用之前文章中实战的demo:webpack-vue-demo,demo已经在webpack的配置文件中配置了“mini-css-extract-plugin”插件:

webpack.config.js

...
//rule的配置
.rule("sass").test( /\.(sass|scss)$/)//sass和scss文件.use("extract-loader")//提取css样式到单独css文件.loader(require('mini-css-extract-plugin').loader).options({hmr: isDev //开发环境开启热载}).end()
...
//插件的配置.plugin("extract-css")//提取css样式到单独css文件.use(require('mini-css-extract-plugin'), [{filename: "css/[name].css",chunkFilename: "css/[name].css"}]).end()

extract-text-webpack-plugin更多的用法和功能就不在这里介绍了,大家自己去看官网mini-css-extract-plugin。

开始

我们在demo的项目根目录执行build命令:

npm run build

然后可以看到输出的dist目录中多了一个css文件:

dist/css/app.css

.app-container[data-v-5ef48958] {color: red;width: 26.66667vw
}body, html {margin: 0;padding: 0
}

这里的css代码其实就是我们项目中抽离出来的css样式,在demo的,

src/app.vue:

<template><div class="app-container">{{ msg }}</div>
</template><script lang="ts">
import { Vue, Component } from "vue-property-decorator";@Component
export default class App extends Vue {msg = "hello world";user = {name: "yasin"};created(): void {// const name = this.user?.name;// console.log("name");}
}
</script><style scoped lang="scss">
.app-container {color: red;width: 200fpx;
}
</style>
<style lang="scss">
html,
body {margin: 0;padding: 0;
}
</style>

可以看到,抽离出来的css样式就是app.vue文件中的style内容。

ok~ 看完了最终打包过后的效果,我们接下来就直接通过源码的角度来分析一下mini-css-extract-plugin插件,看它是怎样把我们app.vue里面的style内容单独抽离到dist/app.css文件中的。

在开始分析之前先上一张我自己总结的webpack的编译流程图:

原理

这里我们提前说几个webpack的知识点:

Dependency

比如我们在代码中使用:

import 'xx.js'或者 require('xx.js')

的时候,webpack就会把“xx.js”看成一个依赖,然后会创建一个Dependency对象对这个依赖进行说明,包含了当前依赖的路径、context上下文等基本信息。

Module

模块,webpack会把一个个依赖创建成一个module对象,也就是说module的创建是依赖dependency对象的,

包含了当前模块的request路径、loaders(加载器集合)、loader加载过后的源码信息、依赖之间的关系等等。

ModuleFactory

模块工厂,正如其名一样“创建模块的工厂”,主要用于module对象的创建。

DependencyTemplate

依赖模版,主要就是把loaders加载过后的代码编译成当前环境,比如浏览器环境能够执行的代码。

第一步:添加CssModuleFactory和CssDependencyTemplate

mini-css-extract-plugin/dist/index.js:

...
apply(compiler) {compiler.hooks.thisCompilation.tap(pluginName, compilation => {//添加CssModuleFactory,CssModuleFactory主要用于把css代码转换成webpack中的module(CssModule)compilation.dependencyFactories.set(_CssDependency.default, new CssModuleFactory());//CssDependencyTemplate主要是将CssModule编译成当前target环境(浏览器)能够执行的代码compilation.dependencyTemplates.set(_CssDependency.default, new CssDependencyTemplate());...

CssModuleFactory:

class CssModuleFactory {create({dependencies: [dependency]}, callback) {//直接返回一个自定义的CssModule对象callback(null, new CssModule(dependency));}}

CssModule:

class CssModule extends _webpack.default.Module {constructor(dependency) {super(MODULE_TYPE, dependency.context);this.id = '';this._identifier = dependency.identifier;this._identifierIndex = dependency.identifierIndex;this.content = dependency.content;this.media = dependency.media;this.sourceMap = dependency.sourceMap;} // no source() so webpack doesn't do add stuff to the bundle...//build方法直接执行callback返回给webpack,告诉webpack当前模块已经加载完成build(options, compilation, resolver, fileSystem, callback) {this.buildInfo = {};this.buildMeta = {};callback();}...}

第二步:在chunk获取清单文件的时候分离出CssModule到单独的file

mini-css-extract-plugin/dist/index.js:

...
//非异步chunk渲染清单文件
compilation.mainTemplate.hooks.renderManifest.tap(pluginName, (result, {chunk}) => {//从当前chunk中分离出所有的CssModuleconst renderedModules = Array.from(chunk.modulesIterable).filter(module => module.type === MODULE_TYPE);if (renderedModules.length > 0) {//在当前chunk的清单文件中添加一个单独的css文件(抽离css样式到单独的file)result.push({render: () => this.renderContentAsset(compilation, chunk, renderedModules, compilation.runtimeTemplate.requestShortener),filenameTemplate: ({chunk: chunkData}) => this.options.moduleFilename(chunkData),pathOptions: {chunk,contentHashType: MODULE_TYPE},identifier: ${pluginName}.${chunk.id},hash: chunk.contentHash[MODULE_TYPE]});}});//异步chunk渲染清单文件compilation.chunkTemplate.hooks.renderManifest.tap(pluginName, (result, {chunk}) => {//从当前chunk中分离出所有的CssModuleconst renderedModules = Array.from(chunk.modulesIterable).filter(module => module.type === MODULE_TYPE);if (renderedModules.length > 0) {//在当前chunk的清单文件中添加一个单独的css文件(抽离css样式到单独的file)result.push({render: () => this.renderContentAsset(compilation, chunk, renderedModules, compilation.runtimeTemplate.requestShortener),filenameTemplate: this.options.chunkFilename,pathOptions: {chunk,contentHashType: MODULE_TYPE},identifier: `${pluginName}.${chunk.id}`,hash: chunk.contentHash[MODULE_TYPE]});}});
...

webpack的异步和非异步模块是什么概念呢?

比如我们使用以下代码:

import("xxx.js") //webpack中的一个异步chunk

这样的异步chunk在生成清单文件的就会走:

//异步chunk渲染清单文件compilation.chunkTemplate.hooks.renderManifest.tap(pluginName, (result, {chunk}) => {//从当前chunk中分离出所有的CssModuleconst renderedModules = Array.from(chunk.modulesIterable).filter(module => module.type === MODULE_TYPE);if (renderedModules.length > 0) {//在当前chunk的清单文件中添加一个单独的css文件(抽离css样式到单独的file)result.push({render: () => this.renderContentAsset(compilation, chunk, renderedModules, compilation.runtimeTemplate.requestShortener),filenameTemplate: this.options.chunkFilename,pathOptions: {chunk,contentHashType: MODULE_TYPE},identifier: `${pluginName}.${chunk.id}`,hash: chunk.contentHash[MODULE_TYPE]});}});

第三步:在loader中添加pitch函数

loader的pitch函数是什么呢?我们在手把手带你撸一遍vue-loader源码文章中有介绍过,vue-loader就是利用了pitch函数进行模块解析的,

比如在上图中,loader2定义了一个pitch函数,如果loader2的pitch函数中有返回值的时候,就会跳过loader3,直接执行排在loader2前面的loader1。

mini-css-extract-plugin/dist/loader.js:

...
function pitch(request) {const options = _loaderUtils.default.getOptions(this) || {};(0, _schemaUtils.default)(_options.default, options, 'Mini CSS Extract Plugin Loader');const loaders = this.loaders.slice(this.loaderIndex + 1);this.addDependency(this.resourcePath);const childFilename = '*';const publicPath = typeof options.publicPath === 'string' ? options.publicPath === '' || options.publicPath.endsWith('/') ? options.publicPath : `${options.publicPath}/` : typeof options.publicPath === 'function' ? options.publicPath(this.resourcePath, this.rootContext) : this._compilation.outputOptions.publicPath;const outputOptions = {filename: childFilename,publicPath};
...return callback(null, resultSource);});
}

第四步:创建一个webpack子编译器编译css依赖

mini-css-extract-plugin/dist/loader.js:

...
//创建一个webpack子编译器const childCompiler = this._compilation.createChildCompiler(`${pluginName} ${request}`, outputOptions);new _NodeTemplatePlugin.default(outputOptions).apply(childCompiler);new _LibraryTemplatePlugin.default(null, 'commonjs2').apply(childCompiler);new _NodeTargetPlugin.default().apply(childCompiler);
//给webpack子编译器设置入口文件(css)new _SingleEntryPlugin.default(this.context, `!!${request}`, pluginName).apply(childCompiler);
...
let source;
//获取webpack子编译器编译过后的代码sourcechildCompiler.hooks.afterCompile.tap(pluginName, compilation => {source = compilation.assets[childFilename] && compilation.assets[childFilename].source(); //因为webpack子编译器编译的内容会写入到webpack主编译器的结果中,所以我们需要删除这一部分内容compilation.chunks.forEach(chunk => {chunk.files.forEach(file => {delete compilation.assets[file]; });});});
//执行webpack子编译器
childCompiler.runAsChild((err, entries, compilation) => {const addDependencies = dependencies => {if (!Array.isArray(dependencies) && dependencies != null) {throw new Error(`Exported value was not extracted as an array: ${JSON.stringify(dependencies)}`);}const identifierCountMap = new Map();for (const dependency of dependencies) {const count = identifierCountMap.get(dependency.identifier) || 0;this._module.addDependency(new _CssDependency.default(dependency, dependency.context, count));identifierCountMap.set(dependency.identifier, count + 1);}};if (err) {return callback(err);}if (compilation.errors.length > 0) {return callback(compilation.errors[0]);}compilation.fileDependencies.forEach(dep => {this.addDependency(dep);}, this);compilation.contextDependencies.forEach(dep => {this.addContextDependency(dep);}, this);if (!source) {return callback(new Error("Didn't get a result from child compiler"));}let locals;try {let dependencies;let exports = evalModuleCode(this, source, request); // eslint-disable-next-line no-underscore-dangleexports = exports.__esModule ? exports.default : exports;locals = exports && exports.locals;if (!Array.isArray(exports)) {dependencies = [[null, exports]];} else {dependencies = exports.map(([id, content, media, sourceMap]) => {const module = findModuleById(compilation.modules, id);return {identifier: module.identifier(),context: module.context,content,media,sourceMap};});}addDependencies(dependencies);} catch (e) {return callback(e);}const esModule = typeof options.esModule !== 'undefined' ? options.esModule : false;const result = locals ? `\n${esModule ? 'export default' : 'module.exports ='} ${JSON.stringify(locals)};` : '';let resultSource = `// extracted by ${pluginName}`;resultSource += options.hmr ? hotLoader(result, {context: this.context,options,locals}) : result;return callback(null, resultSource);});
...

第五步:添加webpack子编译器的结果到webpack主编译器modules中

mini-css-extract-plugin/dist/loader.js:

//执行webpack子编译器
childCompiler.runAsChild((err, entries, compilation) => {//添加依赖到webpack主编译器const addDependencies = dependencies => {...const count = identifierCountMap.get(dependency.identifier) || 0;//根据子编译器结果中的dependency创建CssDependency到主编译器中this._module.addDependency(new _CssDependency.default(dependency, dependency.context, count));identifierCountMap.set(dependency.identifier, count + 1);}};...try {let dependencies;//执行webpack子编译器生成的结果,获取被webpack子编译器编译过后的css代码                   let exports = evalModuleCode(this, source, request); exports = exports.__esModule ? exports.default : exports;locals = exports && exports.locals;//根据结果创建dependency对象if (!Array.isArray(exports)) {dependencies = [[null, exports]];} else {dependencies = exports.map(([id, content, media, sourceMap]) => {const module = findModuleById(compilation.modules, id);return {identifier: module.identifier(),context: module.context,content,media,sourceMap};});}//添加dependency到webpack主编译器addDependencies(dependencies);} catch (e) {return callback(e);}

第六步:在chunk中分离出所有的CssModule到单独文件

也就是会执行前面插件中的钩子函数,

mini-css-extract-plugin/dist/index.js:

...
//非异步chunk渲染清单文件
compilation.mainTemplate.hooks.renderManifest.tap(pluginName, (result, {chunk}) => {//从当前chunk中分离出所有的CssModuleconst renderedModules = Array.from(chunk.modulesIterable).filter(module => module.type === MODULE_TYPE);if (renderedModules.length > 0) {//在当前chunk的清单文件中添加一个单独的css文件(抽离css样式到单独的file)result.push({render: () => this.renderContentAsset(compilation, chunk, renderedModules, compilation.runtimeTemplate.requestShortener),filenameTemplate: ({chunk: chunkData}) => this.options.moduleFilename(chunkData),pathOptions: {chunk,contentHashType: MODULE_TYPE},identifier: ${pluginName}.${chunk.id},hash: chunk.contentHash[MODULE_TYPE]});}});//异步chunk渲染清单文件compilation.chunkTemplate.hooks.renderManifest.tap(pluginName, (result, {chunk}) => {//从当前chunk中分离出所有的CssModuleconst renderedModules = Array.from(chunk.modulesIterable).filter(module => module.type === MODULE_TYPE);if (renderedModules.length > 0) {//在当前chunk的清单文件中添加一个单独的css文件(抽离css样式到单独的file)result.push({render: () => this.renderContentAsset(compilation, chunk, renderedModules, compilation.runtimeTemplate.requestShortener),filenameTemplate: this.options.chunkFilename,pathOptions: {chunk,contentHashType: MODULE_TYPE},identifier: `${pluginName}.${chunk.id}`,hash: chunk.contentHash[MODULE_TYPE]});}});//拼接所有的CssModule中的源码renderContentAsset(compilation, chunk, modules, requestShortener) {...//遍历所有的CssModule,拼接所有的CssModule中的源码for (const m of usedModules) {if (/^@import url/.test(m.content)) {// HACK for IE// http://stackoverflow.com/a/14676665/1458162let {content} = m;if (m.media) {// insert media into the @import// this is rar// TODO improve this and parse the CSS to support multiple mediascontent = content.replace(/;|\s*$/, m.media);}externalsSource.add(content);externalsSource.add('\n');} else {if (m.media) {source.add(`@media ${m.media} {\n`);}if (m.sourceMap) {source.add(new SourceMapSource(m.content, m.readableIdentifier(requestShortener), m.sourceMap));} else {source.add(new OriginalSource(m.content, m.readableIdentifier(requestShortener)));}source.add('\n');if (m.media) {source.add('}\n');}}}return new ConcatSource(externalsSource, source);}
...

ok! 整个mini-css-extract-plugin流程我们就分析完毕了,还有一些细节的内容没有展示出来了,小伙伴自己去看源码哦~

总结

“mini-css-extract-plugin”插件其实就是充分利用了webpack编译过程中的钩子函数,对特定编译过程进行处理,所以只有完全理解webpack编译过程才能写出牛逼的插件来,就像写“mini-css-extract-plugin”的作者一样,肯定是已经啃透了webpack源码,已经可以运用自如了,唉唉,差距还是比较大呀,不然我也不会在这里分析别人的源码了,哈哈!加油吧~

最后欢迎志同道合的人一起学习,一起交流!!

mini-css-extract-plugin源码解析相关推荐

  1. Kubernetes学习笔记之Calico CNI Plugin源码解析(二)

    女主宣言 今天小编继续为大家分享Kubernetes Calico CNI Plugin学习笔记,希望能对大家有所帮助. PS:丰富的一线技术.多元化的表现形式,尽在"360云计算" ...

  2. 购物车(js+css+html)源码解析

    购物车源码解析 先取得表格: Js代码 1.var table = document.getElementById("table"); 然后遍历表格的行数进行删除: Js代码 1. ...

  3. Android Gradle Plugin 源码解析(上)

    一.源码依赖 本文基于: android gradle plugin版本: com.android.tools.build:gradle:2.3.0 gradle 版本:4.1 Gradle源码总共3 ...

  4. Android Gradle Plugin 源码解析之 externalNativeBuild

    在Android Studio 2.2开始的Android Gradle Plugin版本中,Google集成了对cmake的完美支持,而原先的ndkBuild的方式支持也变得更加良好.这篇文章就来说 ...

  5. Kubernetes学习笔记之Calico CNI Plugin源码解析(一)

    女主宣言 今天小编为大家分享Kubernets Calico CNI Plugin的源码学习笔记,希望对正在学习k8s相关部分的同学有所帮助: PS:丰富的一线技术.多元化的表现形式,尽在" ...

  6. Cilium创建pod network源码解析

    01 Overview 我们生产K8s使用容器网络插件 Cilium 来创建 Pod network,下发 eBPF 程序实现 service 负载均衡来替换 kube-proxy,并且使用 BGP ...

  7. 基于postCss的TaiWindCss源码解析

    基于postCss的TaiWindCss源码解析 前言 了解 postCss 什么 是 postCss? postCss的核心原理/工作流 TaiWindCss 源码解析 TaiWindCss 是什么 ...

  8. webpack那些事:浅入深出-源码解析构建优化

    基础知识回顾 入口(entry) module.exports = {entry: './path/to/my/entry/file.js' }; //或者 module.exports = {ent ...

  9. mpx脚手架mpx-template模板源码解析

    前言 mpx脚手架中使用的模板为mpx-template,里面做了一些配置化的东西,如果了解源码后,可以自定义模板和脚手架. git地址(2019年12月19日版本):https://github.c ...

最新文章

  1. hibernate 多对一(Many-to-one)单向关联
  2. linux 编译git 客户端源码然后安装
  3. 刚刚有水了一道,用递归实现进制转换
  4. 简单的eda实验vga在linux系统中,EDA实验报告-VGA彩条显示.doc
  5. python中类型错误、计数不采用关键字的错误怎么改_Python设计错误
  6. 基于阿尔法贝塔剪枝算法的五子棋_C4.5算法剪枝2
  7. 一篇google developer 安全介绍的翻译
  8. SLAM会议笔记(一)LOAM
  9. 从C#程序中调用非受管DLLs
  10. 多摩川绝对值编码器STM32F103通信源码 通信实现源码及硬件实现方案,用于伺服行业开发者开发编码器接口,对于使用STM32开发电流环的人员具有参考价值
  11. 网络层-1、网络层功能概述
  12. 远程桌面无法连接解决方法
  13. 十六进制表示法(二进制/十六进制/十进制之间的转换)
  14. 节点重要度 matlab,复杂网络节点重要度常用指标及其计算
  15. Windows上哪款卸载软件最值得推荐?
  16. 服务器定时任务是通过什么样的方式实现的
  17. 变色玫瑰html,玫瑰花变色实验
  18. 五十一个经典小故事4
  19. zeek(bro) 脚本学习 二
  20. MS Office Excel 2007/2003 资料下载汇总

热门文章

  1. 小米node2红外_智能家居之一:小米人体传感器2使用体验
  2. 如何在公众号添加视频链接
  3. 解决项目部署到阿里云服务器邮件发送失败的方法
  4. 何时“大庇天下寒士俱欢颜”(附笑话)
  5. 【Linux】工具使用
  6. 5G Abbreviations(5G中简写和缩略语含义)
  7. 全网最便宜的OpenHarmony开发板和模组Neptune问世(基于联盛德W800的SoC),9.9元带蓝牙和wifi功能还包邮
  8. 来钱快的3种副业,虽然不起眼,不过很赚钱‍‍‍
  9. [智能车]平衡车/直立车的入门经验(代码讲解)
  10. MySQL函数关键字(五)子查询 ANY/SOME/ALL/IN/EXISTS/USING