style-loader|style-loader, 我以为我懂了,其实我错了

关于样式编译
因为webpack编译的思想是万无皆可JS,意旨所有web项目关联的资源文件,都可以通过js关联起来。然而又由于图片,样式这些本来和js八竿子打不到一起的,所以就有了各种loader来解决他们的关联性问题;
说到webpack的样式编译,总有几个loader是不能错过的,比如less-loader, css-loader,style-loader,随便上一段常用配置:

const lessLoader = { test: /\.less$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, }, }, 'less-loader', ], }

【style-loader|style-loader, 我以为我懂了,其实我错了】以上文件就是告诉webpack,在遇到文件名以.less结尾的文件,先用less-loader编译,再用css-loader编译,最后用style-loader,曾经我是用这样一段js伪代码来概述的:
// 伪代码 const res = styleLoader(cssLoader(lessLoader('xxx.less')));

其实这个伪代码,一点都不严谨.
都是loader,但作用却大相径庭
style-loader 其实与 css-loader, less-loader的作用是有区别的,后者其实承担的是模块化与语法转译这一块;而style-loader这一类(还有常用的mini-css-extract-plugin)承担的是粘结剂功能,就是将js中的css加载到html中,从而使样式生效;
style-loader|style-loader, 我以为我懂了,其实我错了
文章图片

随意严谨一点的伪代码应该这样写:
const cssContent = cssLoader(lessLoader('xxx.less')); // 通过styleTag插入css 字符串到head中 styleLoader(cssContent,document.head);

好像文章写到这里就应该结束了,但实际上好戏才开始。
曾经我以为,style-loader是在构建时,就将样式插入到了html中,但实际上我错了,而且这一错就是五年(曾经你以为的,不过是你的无知:送给自己),正确的答案是css被构建到了js文件中,然后在js文件加载时,通过style-loader提供的方法将其加载到html中。
简单来讲,曾经我以为这个loader是个构建时,但他其实是一个运行时。
怎么判断呢,很简单,就像如何判断一个网站是否是服务端渲染一样,看网站首页请求获取的html文件是否含样式(也可以直接通过source面板查看html文件, 不是elements)。
之所以会产生这个误判,是我直接平移了mini-css-extract-plugin的构建思想,这个plugin就剥离了JS中的样式,形成了一个新的文件,然后再把样式地址插入到html头中,这是个完完全全的构建时。
而最近为什么会突然关注到这个点,是因为我们在调一个微组件的方案,在组件成功加载时,发现样式丢失,看看下图的惨不忍睹:
style-loader|style-loader, 我以为我懂了,其实我错了
文章图片

经过debug,发现样式确实被打包进了文件,只是没有被加载到style标签中,第一直觉就是缺少loader,而后查看构建工具,发现样式编译配置只到了css-loader这一级,缺少style-loader
从构建结果看样式加载过程
如果到这文章就结束了,似乎就太不像我了,毕竟刨根问底(xuxudaodao)的我才是真的我.接下来,通过一段实例代码,来看webpack构建,是怎样实现样式加载的。
示例代码:
// style.less代码 @font-size: 28px; .Demo { box-sizing: border-box; :global { .demo-title { font-size: @font-size; } } }

import React from 'react'; // 引入样式 import style from './style.less'; export default function Demo() { return (this is a demo
); }

然后通过webpack,采用style-loader的方式构建,为了方便阅读,没有做代码压缩和丑化。
然后从入口可以看见样式引入是这样的:
// 注释无用代码有删减 __webpack_require__.d(__webpack_exports__, "default", function() { return Demo; }); var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react"); var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); // 引入样式 var _style_less__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/demo/style.less"); var _style_less__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_style_less__WEBPACK_IMPORTED_MODULE_1__); function Demo() { return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", { className: _style_less__WEBPACK_IMPORTED_MODULE_1___default.a.Demo }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h3", { className: "emo-title" }, "this is a demo")); }

可以看见,在入口文件,有一个从./src/demo/style.less模块的样式引入,并赋值给了_style_less__WEBPACK_IMPORTED_MODULE_1___default变量;
然后顺藤摸瓜,继续看一下style.less模块长什么样:
// "./src/demo/style.less" 模块 function(module, exports, __webpack_require__) { var api = __webpack_require__("../../../../.def/def_modules/.builders/@ali/builder-cook/node_modules/_style-loader@1.3.0@style-loader/dist/runtime/injectStylesIntoStyleTag.js"); var content = __webpack_require__("../../../../.def/def_modules/.builders/@ali/builder-cook/node_modules/_css-loader@1.0.1@css-loader/index.js?!../../../../.def/def_modules/.builders/@ali/builder-cook/node_modules/_postcss-loader@3.0.0@postcss-loader/src/index.js?!../../../../.def/def_modules/.builders/@ali/builder-cook/node_modules/_less-loader@6.2.0@less-loader/dist/cjs.js?!./src/demo/style.less"); content = content.__esModule ? content.default : content; if (typeof content === 'string') { content = [[module.i, content, '']]; }var options = {}; options.insert = "head"; options.singleton = false; var update = api(content, options); module.exports = content.locals || {}; }

这这一块的代码就可以看出,这一个模块是一个承上(css引入)启下(插入html);
承上通过从less-loader/dist/cjs.js?!./src/demo/style.less这个模块引入, 而插入html则是通过从injectStylesIntoStyleTag导入了一个方法,从方法名就可以知道他的作用就是通过style标签插入css样式.
这里面还有一个点,就是最后的导出,模块最后将content.locals导出,继续根据线索去看看content到底是什么:
function(module, exports, __webpack_require__) { exports = module.exports = __webpack_require__("../../../../.def/def_modules/.builders/@ali/builder-cook/node_modules/_css-loader@1.0.1@css-loader/lib/css-base.js")(false); exports.push([module.i, "._1rfoVjSya7b-iuQB2_qKPh {\n-webkit-box-sizing: border-box; \nbox-sizing: border-box; \n}\n._1rfoVjSya7b-iuQB2_qKPh .demo-title {\nfont-size: 28px; \n}\n", ""]); exports.locals = { "Demo": "_1rfoVjSya7b-iuQB2_qKPh" }; }

通过上面的代码,就可以看出exports.locals的作用就是css的模块化
至此,真相大白....
写在最后
我似乎特别享受这种顺藤摸瓜的前端(ruozhi)侦探感觉, 这让以前对我来说的一个黑盒被打开了,最后还是那句话,前端里面真的没什么黑魔化,基本就是红宝书里面的知识,只要能静下心来细品: 魔法终究只是一个特别好的想法而已。
style-loader|style-loader, 我以为我懂了,其实我错了
文章图片

    推荐阅读