首先打开style-loader的package.json,找到main,能够看到它的入口文件即为:dist/index.js,内容以下:`css
var _path = _interopRequireDefault(require("path")); var _loaderUtils = _interopRequireDefault(require("loader-utils")); var _schemaUtils = _interopRequireDefault(require("schema-utils")); var _options = _interopRequireDefault(require("./options.json")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } module.exports = () => {}; module.exports.pitch = function loader(request) { // ... }
`其中_interopRequireDefault的做用是:若是引入的是 es6 模块,直接返回,若是是 commonjs 模块,则将引入的内容放在一个对象的 default 属性上,而后返回这个对象。
我首先来看pitch函数,它的内容以下:`webpack
// 获取webpack配置的options const options = _loaderUtils.default.getOptions(this) || {}; // (0, func)(),运用逗号操做符,将func的this指向了windows,详情请查看:https://www.jianshu.com/p/cd188bda72df // 调用_schemaUtils是为了校验options,知道其做用就行,这里就不讨论了 (0, _schemaUtils.default)(_options.default, options, { name: 'Style Loader', baseDataPath: 'options' }); // 定义了两个变量,**insert**、**injectType**,不难看出insert的默认值为head,injectType默认值为styleTag const insert = typeof options.insert === 'undefined' ? '"head"' : typeof options.insert === 'string' ? JSON.stringify(options.insert) : options.insert.toString(); const injectType = options.injectType || 'styleTag'; switch(injectType){ case 'linkTag': { // ... } case 'lazyStyleTag': case 'lazySingletonStyleTag': { // ... } case 'styleTag': case 'singletonStyleTag': default: { // ... } }`
在这里,咱们就看默认的就行了,即insert=head,injectType=styleTag`es6
const isSingleton = injectType === 'singletonStyleTag'; const hmrCode = this.hot ? ` // ... ` : ''; return ` // _loaderUtils.default.stringifyRequest这里就不叙述了,主要做用是将绝对路径转换为相对路径 var content = require(${_loaderUtils.default.stringifyRequest(this, `!!${request}`)}); if (typeof content === 'string') { content = [[module.id, content, '']]; } var options = ${JSON.stringify(options)} options.insert = ${insert}; options.singleton = ${isSingleton}; var update = require(${_loaderUtils.default.stringifyRequest(this, `!${_path.default.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)})(content, options); if (content.locals) { module.exports = content.locals; } ${hmrCode} `;`
去掉多余的代码,能够清晰的看到pitch方法实际上最后返回了一个字符串,该字符串就是编译后在浏览器执行的代码,让咱们来看看它在浏览器是如何操做的:
首先调用require方法获取css文件的内容,将其赋值给content,若是content是字符串,则将content赋值为数组,即:[[module.id], content, ''],接着咱们覆盖了options的insert、singleton属性,因为咱们暂时只看默认的,因此insert=head,singleton=false;再往下面看,咱们又使用require方法引用了runtime/injectStyleIntoStyleTag.js,它返回一个函数,咱们将content和options传递给该函数,并当即执行它:`web
module.exports = function (list, options) { options = options || {}; options.attributes = typeof options.attributes === 'object' ? options.attributes : {}; // Force single-tag solution on IE6-9, which has a hard limit on the # of <style> // tags it will allow on a page if (!options.singleton && typeof options.singleton !== 'boolean') { options.singleton = isOldIE(); } var styles = listToStyles(list, options); addStylesToDom(styles, options); return function update(newList) { // ... }; };
能够看到,该函数的主要内容即为
json
var styles = listToStyles(list, options); addStylesToDom(styles, options);
咱们先来看看listToStyles作了什么
windows
function listToStyles(list, options) { var styles = []; var newStyles = {}; for (var i = 0; i < list.length; i++) { var item = list[i]; // 回过头去看就知道,item实际上等于[[module.id, content, '']],其中content即为css文件的内容 var id = options.base ? item[0] + options.base : item[0]; var css = item[1]; var media = item[2]; // '' var sourceMap = item[3]; // undefined var part = { css: css, media: media, sourceMap: sourceMap }; if (!newStyles[id]) { styles.push(newStyles[id] = { id: id, parts: [part] }); } else { newStyles[id].parts.push(part); } } return styles; }
这段代码很简单,将传递进来的内容转换为了styles数组,接下来看看addStylesToDom函数:
数组
// 在文件顶部,定义了stylesInDom对象,主要是用来记录已经被加入DOM中的styles var stylesInDom = {}; function addStylesToDom(styles, options) { for (var i = 0; i < styles.length; i++) { var item = styles[i]; var domStyle = stylesInDom[item.id]; var j = 0; // 判断当前style是否加入DOM中 if (domStyle) { domStyle.refs++; // 若是加入,首先循环已加入DOM的parts,并调用其函数,这里咱们比较疑惑,可是往下看两行咱们就知道这个函数从哪儿来了 for (; j < domStyle.parts.length; j++) { domStyle.parts[j](item.parts[j]); } // 除了上面循环的,若是传进来的style还有则说明又新增的,调用addStyle方法并将其返回值放入domStyle的parts中 // 这里就知道了parts中存放的是addStyle,且是一个函数 for (; j < item.parts.length; j++) { domStyle.parts.push(addStyle(item.parts[j], options)); } } else { // 若是没有加入DOM中,则依次调用addStyle并存入数组parts中,并将当前的style存入stylesInDom对象中 var parts = []; for (; j < item.parts.length; j++) { parts.push(addStyle(item.parts[j], options)); } stylesInDom[item.id] = { id: item.id, refs: 1, parts: parts }; } } }
其中的关键仍是在于addStyle函数
浏览器
var singleton = null; var singletonCounter = 0; function addStyle(obj, options) { var style; var update; var remove; // 默认singleton为false,因此暂时不考虑if的内容了 if (options.singleton) { var styleIndex = singletonCounter++; style = singleton || (singleton = insertStyleElement(options)); update = applyToSingletonTag.bind(null, style, styleIndex, false); remove = applyToSingletonTag.bind(null, style, styleIndex, true); } else { style = insertStyleElement(options); update = applyToTag.bind(null, style, options); remove = function remove() { removeStyleElement(style); }; } update(obj); return function updateStyle(newObj) { if (newObj) { if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap) { return; } update(obj = newObj); } else { remove(); } }; }
能够看到它返回一个函数,其主要内容是判断传入的对象是否与原对象相等,若是相等,则什么都不作,不然调用update函数,若是对象为空,则调用remove函数。而update与remove是在else中被赋值的,在赋值以前,咱们首先看insertStyleElement函数:
app
var getTarget = function getTarget() { var memo = {}; return function memorize(target) { if (typeof memo[target] === 'undefined') { var styleTarget = document.querySelector(target); // Special case to return head of iframe instead of iframe itself if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) { try { // This will throw an exception if access to iframe is blocked // due to cross-origin restrictions styleTarget = styleTarget.contentDocument.head; } catch (e) { // istanbul ignore next styleTarget = null; } } memo[target] = styleTarget; } return memo[target]; }; }(); function insertStyleElement(options) { var style = document.createElement('style'); if (typeof options.attributes.nonce === 'undefined') { var nonce = typeof __webpack_nonce__ !== 'undefined' ? __webpack_nonce__ : null; if (nonce) { options.attributes.nonce = nonce; } } Object.keys(options.attributes).forEach(function (key) { style.setAttribute(key, options.attributes[key]); }); if (typeof options.insert === 'function') { options.insert(style); } else { var target = getTarget(options.insert || 'head'); if (!target) { throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid."); } target.appendChild(style); } return style; }
上面函数很简单,建立一个style标签,并将其插入insert中,即head中,回到以前的地方,咱们定义了update和remove,以后咱们手动调用update函数,即applyToTag
dom
function applyToTag(style, options, obj) { var css = obj.css; var media = obj.media; var sourceMap = obj.sourceMap; if (media) { style.setAttribute('media', media); } if (sourceMap && btoa) { css += "\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), " */"); } // For old IE /* istanbul ignore if */ if (style.styleSheet) { style.styleSheet.cssText = css; } else { while (style.firstChild) { style.removeChild(style.firstChild); } style.appendChild(document.createTextNode(css)); } }
这段代码很简单,即给刚建立的style标签更新内容,而remove函数指向removeStyleElement函数
function removeStyleElement(style) { // istanbul ignore if if (style.parentNode === null) { return false; } style.parentNode.removeChild(style); }
`即删除styleDOM结构
总结一下,style-loader会返回一个字符串,而在浏览器中调用时,会将建立一个style标签,将其加入head中,并将css的内容放入style中,同时每次该文件更新也会相应的更新Style结构,若是该css文件内容被删除,则style的内容也会被相应的删除,整体来讲,style-loader作了一件很是简单的事:在 DOM 里插入一个 <style> 标签,而且将 CSS 写入这个标签内`
const style = document.createElement('style'); // 新建一个 style 标签 style.type = 'text/css’; style.appendChild(document.createTextNode(content)) // CSS 写入 style 标签 document.head.appendChild(style); // style 标签插入 head 中
`