webpack loader源码解析系列(四)-css-loader、style-loader

文章目录

  • 示例文件
  • css-loader
  • style-loader

示例文件

// index.css 入口文件
@import url('./1.css');

* {
    margin: 0;
}

// 1.css
@import url('./2.css');
@import './3.css';

.f {
    color: red;
}

// 2.css
.f {
    background-color: red;
}

// 3.css
.f {
    font-weight: 100;
}
// index.js
import cssArr from './css/index.css' 

console.log(cssArr)

css-loader

  • 作用:将 css 里的 @import、url() 等语法解析成 webpack 能够识别的语法,语法分析根据 postcss 插件实现
  • 最终 index.css 通过 css-loader 解析后输出内容:数组每个索引的位置信息将在 style-loade 中解析
    webpack loader源码解析系列(四)-css-loader、style-loader_第1张图片
async function loader(content, map, meta) {
  // 获取配置信息
  const rawOptions = this.getOptions(_options.default);
  const plugins = [];
  const callback = this.async();
  let options;
  try {
    /**
      {
        url: true,
        import: true,
        modules: false,
        sourceMap: true,
        importLoaders: undefined,
        esModule: true,
        exportType: "array",
      }
    */
    options = (0, _utils.normalizeOptions)(rawOptions, this);
  } catch (error) {
    callback(error);
    return;
  }
  
  // ...
  
  // 处理通过 postcss 插件处理相关语句	
  const importPluginImports = []; // 转译源码所需的postcss插件
  shouldUseModulesPlugins(options, this) && plugins.push(getModulesPlugins); // 处理css-modules
  shouldUseImportPlugin(options, this) && plugins.push(importParser); // 处理@import语句
  shouldUseURLPlugin(options, this) && plugins.push(urlParser); // 处理url()语句
  shouldUseIcssPlugin(options, this) && plugins.push(icssParser); // 处理icss相关逻辑
  
 // 复用其他 loader 传递的 ast,比如 postcss-loader 
   if (meta) {
    const {
      ast
    } = meta;
    if (ast && ast.type === "postcss") {
      // eslint-disable-next-line no-param-reassign
      content = ast.root;
    }
  }
  
  // 使用 postcss 开始转译
  const result = await postcss(plugins).process(content); 
  
  // 生成导入代码
  const importCode = getImportCode();
  // 生成内容代码
  const moduleCode = getModuleCode(result);
  // 生成导出代码
  const exportCode = getExportCode(); 
  
  callback(null, `${importCode}${moduleCode}${exportCode}`);
}
  • 通过 css-loader 解析后,index.css 将变成
 
// Imports
import ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";
import ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";
// 将 @import url('./1.css'); 解析后的结果
// 注意这里通过内联 css-loader 调用,-! 表示禁用 preLoader 和 normalLoader
import ___CSS_LOADER_AT_RULE_IMPORT_0___ from \"-!../../node_modules/css-loader/dist/cjs.js!./1.css\";

// 即 api(sourceMaps)
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);
// 将 1.css 解析后的模块添加进 ___CSS_LOADER_EXPORT___ 导出,模块解析后的内容都是下面 push 数组中的格式
___CSS_LOADER_EXPORT___.i(___CSS_LOADER_AT_RULE_IMPORT_0___);

// Module
___CSS_LOADER_EXPORT___.push([module.id, \"* {\\n    margin: 0;\\n}\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/css/index.css\"],\"names\":[],\"mappings\":\"AAEA;IACI,SAAS;AACb\",\"sourcesContent\":[\"@import url('./1.css');\\n\\n* {\\n    margin: 0;\\n}\"],\"sourceRoot\":\"\"}]);

// Exports
export default ___CSS_LOADER_EXPORT___;
  • 运行时API sourceMaps 和 api

api.js

  • toString:输出完整的css字符串
  • i:将postcss解析完成的其他模块push进list中,具体格式查看开头的输出结果
"use strict";

/*
  MIT License http://www.opensource.org/licenses/mit-license.php
  Author Tobias Koppers @sokra
*/
module.exports = function (cssWithMappingToString) {
  var list = [];

  // 调用 toString 方法,将 postcss 解析后的结果拼接成完整的css样式字符串,主要存在 @media 等进行拼接
  list.toString = function toString() {
    return this.map(function (item) {
      var content = "";
      var needLayer = typeof item[5] !== "undefined";
      if (item[4]) {
        content += "@supports (".concat(item[4], ") {");
      }
      if (item[2]) {
        content += "@media ".concat(item[2], " {");
      }
      if (needLayer) {
        content += "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {");
      }
      content += cssWithMappingToString(item);
      if (needLayer) {
        content += "}";
      }
      if (item[2]) {
        content += "}";
      }
      if (item[4]) {
        content += "}";
      }
      return content;
    }).join("");
  };

  // import a list of modules into the list
  list.i = function i(modules, media, dedupe, supports, layer) {
    if (typeof modules === "string") {
      modules = [[null, modules, undefined]];
    }
    var alreadyImportedModules = {};
    if (dedupe) {
      for (var k = 0; k < this.length; k++) {
        var id = this[k][0];
        if (id != null) {
          alreadyImportedModules[id] = true;
        }
      }
    }
    for (var _k = 0; _k < modules.length; _k++) {
      var item = [].concat(modules[_k]);
      if (dedupe && alreadyImportedModules[item[0]]) {
        continue;
      }
      if (typeof layer !== "undefined") {
        if (typeof item[5] === "undefined") {
          item[5] = layer;
        } else {
          item[1] = "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {").concat(item[1], "}");
          item[5] = layer;
        }
      }
      if (media) {
        if (!item[2]) {
          item[2] = media;
        } else {
          item[1] = "@media ".concat(item[2], " {").concat(item[1], "}");
          item[2] = media;
        }
      }
      if (supports) {
        if (!item[4]) {
          item[4] = "".concat(supports);
        } else {
          item[1] = "@supports (".concat(item[4], ") {").concat(item[1], "}");
          item[4] = supports;
        }
      }
      list.push(item);
    }
  };
  return list;
};

sourceMaps

  • 在 css 字符串后内联 sourceMap
"use strict";

module.exports = function (item) {
  var content = item[1];
  var cssMapping = item[3];
  if (!cssMapping) {
    return content;
  }
  if (typeof btoa === "function") {
    // btoa 将字符串转为 base64 编码的 ASCII 字符串。
    var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(cssMapping))));
    var data = "sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(base64);
    var sourceMapping = "/*# ".concat(data, " */");
    return [content].concat([sourceMapping]).join("\n");
  }
  return [content].join("\n");
};

style-loader

  • 拿到 css-loader 中解析的 css 模块返回的数组格式内容,拼接成完整字符串然后添加进 style 标签里插入 head 标签中
const loaderAPI = () => {};
loaderAPI.pitch = function loader(request) {
  // request 为后面的内联信息 "/xxx/webpack/wb/node_modules/css-loader/dist/cjs.js !/xxx/webpack/wb/src/css/index.css"
  
  // 解析options请求
  const options = this.getOptions(schema);
  // ...
  switch (injectType) {
  	// ...
  	
  	// 默认会走这里
  	case "styleTag":
    case "autoStyleTag":
    case "singletonStyleTag":
    default:
      {
        const isSingleton = injectType === "singletonStyleTag";
        const isAuto = injectType === "autoStyleTag";
        const hmrCode = this.hot ? (0, _utils.getStyleHmrCode)(esModule, this, request, false) : "";
        
        const res= `
        ${(0, _utils.getImportStyleAPICode)(esModule, this)}
        ${(0, _utils.getImportStyleDomAPICode)(esModule, this, isSingleton, isAuto)}
        ${(0, _utils.getImportInsertBySelectorCode)(esModule, this, insertType, options)}
        ${(0, _utils.getSetAttributesCode)(esModule, this, options)}
        ${(0, _utils.getImportInsertStyleElementCode)(esModule, this)}
        ${(0, _utils.getStyleTagTransformFnCode)(esModule, this, options, isSingleton, styleTagTransformType)}
        ${(0, _utils.getImportStyleContentCode)(esModule, this, request)}
        ${isAuto ? (0, _utils.getImportIsOldIECode)(esModule, this) : ""}
        ${esModule ? "" : `content = content.__esModule ? content.default : content;`}

        var options = ${JSON.stringify(runtimeOptions)};

        ${(0, _utils.getStyleTagTransformFn)(options, isSingleton)};
        options.setAttributes = setAttributes;
        ${(0, _utils.getInsertOptionCode)(insertType, options)}
        options.domAPI = ${(0, _utils.getdomAPI)(isAuto)};
        options.insertStyleElement = insertStyleElement;

        var update = API(content, options);

        ${hmrCode}

        ${(0, _utils.getExportStyleCode)(esModule, this, request)}
        `;
        return res;
     }
  }
}
  • 解析拼接请求路径的方法
function stringifyRequest(loaderContext, request) {
  if (typeof loaderContext.utils !== "undefined" && typeof loaderContext.utils.contextify === "function") {
    return JSON.stringify(loaderContext.utils.contextify(loaderContext.context, request));
  }
  // ...
}
function getImportLinkAPICode(esModule, loaderContext) {
  const modulePath = stringifyRequest(loaderContext, `!${_path.default.join(__dirname, "runtime/injectStylesIntoLinkTag.js")}`);
  return esModule ? `import API from ${modulePath};` : `var API = require(${modulePath});`;
}
function getImportLinkContentCode(esModule, loaderContext, request) {
  const modulePath = stringifyRequest(loaderContext, `!!${request}`);
  return esModule ? `import content from ${modulePath};` : `var content = require(${modulePath});`;
}
function getImportStyleAPICode(esModule, loaderContext) {
  // "!../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js"
  const modulePath = stringifyRequest(loaderContext, `!${_path.default.join(__dirname, "runtime/injectStylesIntoStyleTag.js")}`);
  return esModule ? `import API from ${modulePath};` : `var API = require(${modulePath});`;
}
  • 最终 default 中内容实际为
 import API from "!../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js";
 import domAPI from "!../../node_modules/style-loader/dist/runtime/styleDomAPI.js";
 import insertFn from "!../../node_modules/style-loader/dist/runtime/insertBySelector.js";
 import setAttributes from "!../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js";
 import insertStyleElement from "!../../node_modules/style-loader/dist/runtime/insertStyleElement.js";
 import styleTagTransformFn from "!../../node_modules/style-loader/dist/runtime/styleTagTransform.js";
 // 注意 !! 禁用自身外所有 loader
 // 通过内联 loader 的方式,获得 css-loader 的返回结果
 import content, * as namedExport from "!!../../node_modules/css-loader/dist/cjs.js!./index.css";
 
 

 var options = {};

 options.styleTagTransform = styleTagTransformFn;
 options.setAttributes = setAttributes;
 
 options.insert = insertFn.bind(null, "head");

 options.domAPI = domAPI;
 options.insertStyleElement = insertStyleElement;

 var update = API(content, options);

 

 export * from "!!../../node_modules/css-loader/dist/cjs.js!./index.css";
 export default content && content.locals ? content.locals : undefined;

API

  • 调用 modulesToDom 记录 css-loader 返回的数组内容等,然后调用 addElementStyle 添加并插入style 标签内容
module.exports = function (list, options) {
  options = options || {};
  list = list || [];
  // 记录 css-loader 返回的每个数组对象的引用关系等,然后将样式添加进 head 标签内
  var lastIdentifiers = modulesToDom(list, options);

  // 接受一个新的样式列表,并使用新样式更新 DOM,同时删除任何不再需要的旧样式。
  return function update(newList) {
    newList = newList || [];
    // 当首次添加样式时,其 references 属性设置为 1。如果再次添加相同的样式,则其 references 属性将递增
    for (var i = 0; i < lastIdentifiers.length; i++) {
      var identifier = lastIdentifiers[i];
      var index = getIndexByIdentifier(identifier);
      // 如果旧样式还在,那么 modulesToDom 中 references 会加回来
      stylesInDOM[index].references--;
    }
    var newLastIdentifiers = modulesToDom(newList, options);
    for (var _i = 0; _i < lastIdentifiers.length; _i++) {
      var _identifier = lastIdentifiers[_i];
      var _index = getIndexByIdentifier(_identifier);
      // 如果其 references 属性达到 0,则可以安全地从 DOM 中删除。
      if (stylesInDOM[_index].references === 0) {
        stylesInDOM[_index].updater();
        stylesInDOM.splice(_index, 1);
      }
    }
    lastIdentifiers = newLastIdentifiers;
  };
};

var stylesInDOM = [];
function getIndexByIdentifier(identifier) {
  var result = -1;
  for (var i = 0; i < stylesInDOM.length; i++) {
    if (stylesInDOM[i].identifier === identifier) {
      result = i;
      break;
    }
  }
  return result;
}
function modulesToDom(list, options) {
  var idCountMap = {};
  var identifiers = [];
  for (var i = 0; i < list.length; i++) {
    var item = list[i];
    var id = options.base ? item[0] + options.base : item[0];
    var count = idCountMap[id] || 0;
    var identifier = "".concat(id, " ").concat(count);
    idCountMap[id] = count + 1;
    var indexByIdentifier = getIndexByIdentifier(identifier);
    var obj = {
      css: item[1], // 纯样式
      media: item[2], // 如果样式中有 @media
      sourceMap: item[3], // 如有 sourceMap
      supports: item[4], // 如果样式中有 @supports
      layer: item[5] // 如果样式中有 @layer
    };
    if (indexByIdentifier !== -1) {
      stylesInDOM[indexByIdentifier].references++;
      stylesInDOM[indexByIdentifier].updater(obj);
    } else {
      var updater = addElementStyle(obj, options);
      options.byIndex = i;
      stylesInDOM.splice(i, 0, {
        identifier: identifier,
        updater: updater,
        references: 1
      });
    }
    identifiers.push(identifier);
  }
  return identifiers;
}
function addElementStyle(obj, options) {
  // 往 head 内插入 style 空标签,并返回添加 style 内容的 update、删除 style 的 remove API
  var api = options.domAPI(options);
  api.update(obj);
  // 返回更新 style 内容的方法,会新内容和之前不相等才会更新
  var updater = function updater(newObj) {
    if (newObj) {
      if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap && newObj.supports === obj.supports && newObj.layer === obj.layer) {
        return;
      }
      api.update(obj = newObj);
    } else {
      api.remove();
    }
  };
  return updater;
}

domAPI

  • addElementStyle 通过 domAPI 实现 dom 操作
  • 通过 apply 拼接完整的 css 字符串和内联 sourceMap
"use strict";

/* istanbul ignore next  */
function apply(styleElement, options, obj) {
  var css = "";
  if (obj.supports) {
    css += "@supports (".concat(obj.supports, ") {");
  }
  if (obj.media) {
    css += "@media ".concat(obj.media, " {");
  }
  var needLayer = typeof obj.layer !== "undefined";
  if (needLayer) {
    css += "@layer".concat(obj.layer.length > 0 ? " ".concat(obj.layer) : "", " {");
  }
  css += obj.css;
  if (needLayer) {
    css += "}";
  }
  if (obj.media) {
    css += "}";
  }
  if (obj.supports) {
    css += "}";
  }
  var sourceMap = obj.sourceMap;
  if (sourceMap && typeof btoa !== "undefined") {
    css += "\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), " */");
  }

  // For old IE
  // 往空 style 标签内添加样式
  options.styleTagTransform(css, styleElement, options.options);
}
function removeStyleElement(styleElement) {
  // istanbul ignore if
  if (styleElement.parentNode === null) {
    return false;
  }
  styleElement.parentNode.removeChild(styleElement);
}

/* istanbul ignore next  */
function domAPI(options) {
  if (typeof document === "undefined") {
    return {
      update: function update() {},
      remove: function remove() {}
    };
  }
  // 往 head 内插入 style 空标签
  var styleElement = options.insertStyleElement(options);
  return {
    // 补充 css 样式、内联 sourceMap 等,然后插入 style 标签内容
    update: function update(obj) {
      apply(styleElement, options, obj);
    },
    remove: function remove() {
      removeStyleElement(styleElement);
    }
  };
}
module.exports = domAPI;

insertStyleElement

  • 创建 style 标签并添加进 head 标签中,注意此时 style 是空内容
function insertStyleElement(options) {
  var element = document.createElement("style");
  options.setAttributes(element, options.attributes);
  options.insert(element, options.options);
  return element;
}
module.exports = insertStyleElement;

insert

  • 实际的插入方法
"use strict";

var memo = {};

/* istanbul ignore next  */
function getTarget(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];
}

/* istanbul ignore next  */
function insertBySelector(insert, style) {
  var target = getTarget(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);
}
module.exports = insertBySelector;

styleTagTransform

  • 补充 style 标签内容
// 往空 style 标签内添加样式
function styleTagTransform(css, styleElement) {
  // ie 环境下的属性
  if (styleElement.styleSheet) {
    styleElement.styleSheet.cssText = css;
  } else {
    // 先清空 style 里的内容再添加
    while (styleElement.firstChild) {
      styleElement.removeChild(styleElement.firstChild);
    }
    styleElement.appendChild(document.createTextNode(css));
  }
}
module.exports = styleTagTransform;

你可能感兴趣的:(webpack,css,javascript)