乐趣区

styleloader源码解析

首先打开 style-loader 的 package.json, 找到 main,可以看到它的入口文件即为:dist/index.js,内容如下:`

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 配置的 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`

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 传递给该函数,并立即执行它:`

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) {// ...};
};

可以看到,该函数的主要内容即为

var styles = listToStyles(list, options);
addStylesToDom(styles, options);

我们先来看看 listToStyles 做了什么

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 函数:

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

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 中

`

退出移动版