style-loader源碼解析

首先打開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函數,即applyToTagdom

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 中

`

相關文章
相關標籤/搜索