webpack loader 從上手到理解系列:style-loader

原文地址javascript

前言

webpack loaders 從上手到理解系列 還有這些:css

什麼是 style-loader

style-loader 的功能就一個,在 DOM 裏插入一個 <style> 標籤,而且將 CSS 寫入這個標籤內。html

簡單來講就是這樣:vue

const style = document.createElement('style'); // 新建一個 style 標籤
style.type = 'text/css';
style.appendChild(document.createTextNode(content)) // CSS 寫入 style 標籤
document.head.appendChild(style); // style 標籤插入 head 中
複製代碼

稍後會詳細分析源碼,看看和咱們的思路是否一致。java

如何使用 style-loader

1. 安裝

npm install style-loader --save-dev
複製代碼

2. 配置 webapck

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(css)$/,
        use: [
          {
            loader: 'style-loader',
            options: {},
          },
          { loader: 'css-loader' },
        ],
      },
    ],
  },
};
複製代碼

平常的開發中處理樣式文件時,通常會使用到 style-loadercss-loader 這兩個 loadernode

關於 style-loaderoptions,這裏就很少說了,見 style-loader options .webpack

3. 引入一個樣式文件

const indexStyle = require('./assets/style/index.css');
複製代碼

4. 見證奇蹟的時刻

webpack
複製代碼

打包完成以後咱們打開 html 頁面,會看到 <head> 裏已經有了 index.css 裏的樣式內容:git

<style> .container { color: red; background: #999999; } .zelda { width: 260px; height: 100px; } </style>
複製代碼

injectType

單獨講一下 injectType 這個配置項,默認值是 styleTag,經過 <style></style> 的形式插入 DOM 中,咱們來看看不一樣的 injectType 的效果。es6

styleTag

默認狀況下,style-loader 每一次處理引入的樣式文件都會在 DOM 上建立一個 <style> 標籤,好比此時引入兩個樣式文件:github

const globalStyle = require('./assets/style/global.css');
const indexStyle = require('./assets/style/index.css');
複製代碼

輸出的 DOM 結構爲:

<style> html, body { height: 100%; } #app { background: #ffffff; } </style>
<style> .container { color: red; } .zelda { width: 260px; height: 100px; } </style>
複製代碼

singletonStyleTag

上面提到默認狀況下有幾個樣式文件就會插入幾個 <style> 標籤,將 injectType 設置爲 singletonStyleTag 可將全部的樣式文件打在同一個 <style> 標籤裏。

// config
{
  test: /\.(css)$/,
  use: [
    {
      loader: 'style-loader',
      options: {
        injectType: 'singletonStyleTag',
      },
    },
    { loader: 'css-loader' },
  ],
}

// js
const globalStyle = require('./assets/style/global.css');
const indexStyle = require('./assets/style/index.css');
複製代碼

輸出的 DOM 結構爲:

<style> html, body { height: 100%; } #app { background: #ffffff; } .container { background: #f5f5f5; } .container { color: red; background: #999999; } .zelda { width: 260px; height: 100px; } </style>
複製代碼

能夠看到,兩個樣式文件的內容都被放到同一個 <style> 標籤裏了,而且是按照咱們引入樣式文件的順序,彷佛還比較符合預期。

linkTag

injectTypelinkTag,會經過 <link rel="stylesheet" href=""> 的形式將樣式插入到 DOM 中,此時 style-loader 接收到的數據應該是樣式文件的地址,因此搭配的 loader 應該是 file-loader 而不是 css-loader

// config
{
  test: /\.(css)$/,
  use: [
    {
      loader: 'style-loader',
      options: {
        injectType: 'linkTag',
      },
    },
    { loader: 'file-loader' },
  ],
}

// js
const globalStyle = require('./assets/style/global.css');
const indexStyle = require('./assets/style/index.css');
複製代碼

輸出的 DOM 結構爲:

<head>
  <link rel="stylesheet" href="f2742027f8729dc63bfd46029a8d0d6a.css">
  <link rel="stylesheet" href="34cd6c668a7a596c4bedad32a39832cf.css">
</head>
複製代碼

lazyStyleTag, lazySingletonStyleTag

這兩種類型的 injectType 區別在於它們是延遲加載的:

// config
{
  test: /\.(css)$/,
  use: [
    {
      loader: 'style-loader',
      options: {
        injectType: 'lazyStyleTag',
      },
    },
    { loader: 'css-loader' },
  ],
}

// js
const globalStyle = require('./assets/style/global.css');
const indexStyle = require('./assets/style/index.css');

// globalStyle.use();
複製代碼

若是僅僅是像上面同樣導入了樣式文件,樣式是不會插入到 DOM 中的,須要手動使用 globalStyle.use() 來延遲加載 global.css 這個樣式文件。

其它的用法就很少說了,自行查看 style-loader

源碼解析

style-loader 主要能夠分爲:

  • 打包階段
  • runtime 階段

打包階段

先看引入依賴部分的代碼:

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 }; }
複製代碼

這裏定義了一個 _interopRequireDefault 方法,傳入的是一個 require()

這個方法的做用是:若是引入的是 es6 模塊,直接返回,若是是 commonjs 模塊,則將引入的內容放在一個對象的 default 屬性上,而後返回這個對象。

module.exports = () => {};
module.exports.pitch = function loader(request) {}
複製代碼

style-loader 的導出方式和普通的 loader 不太同樣,默認導出一個空方法,經過 pitch 導出的。

默認的 loader 都是從右向左像管道同樣執行,而 pitch 是從左到右執行的。

爲何 style-loader 須要這樣呢?

咱們知道默認 loader 的執行是從右向左的,而且會將上一個 loader 處理的結果傳遞給下一個 loader,若是按照這種默認行爲,css-loader 會返回一個 js 字符串給 style-loader

style-loader 的做用是將 CSS 代碼插入到 DOM 中,若是按照順序從 css-loader 接收到一個 js 字符串的話,就沒法獲取到真實的 CSS 樣式了。因此正確的作法是先執行 style-loader,在它裏面去執行 css-loader ,拿到通過處理的 CSS 內容,再插入到 DOM 中。

接下來看看 loader 的內容:

// 獲取 webpack 配置裏的 options
const options = _loaderUtils.default.getOptions(this) || {};
// 校驗 options
(0, _schemaUtils.default)(_options.default, options, {
  name: 'Style Loader',
  baseDataPath: 'options'
});

// style 標籤插入的位置,默認是 head
const insert = typeof options.insert === 'undefined' ? '"head"' : typeof options.insert === 'string' ? JSON.stringify(options.insert) : options.insert.toString();
// 設置以哪一種方式插入 DOM 中
// 詳情見這個:https://github.com/webpack-contrib/style-loader#injecttype
const injectType = options.injectType || 'styleTag';

switch (injectType) {
  case 'linkTag': {}
  case 'lazyStyleTag':
  case 'lazySingletonStyleTag': {}
  case 'styleTag':
  case 'singletonStyleTag':
  default: {}
}
複製代碼

根據不一樣的 injectTypereturn 不一樣的 js 代碼,在 runtime 的時候執行。

看看默認狀況:

return `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}`;
複製代碼

_loaderUtils.default.stringifyRequest(this, `!!${request}`) 這個方法的做用是將絕對路徑轉換成相對路徑。好比:

import css from './asset/style/global.css';
// 此時傳遞給 style-loader 的 request 會是
request = '/test-loader/node_modules/css-loader/dist/cjs.js!/test-loader/assets/style/global.css';
// 轉換
_loaderUtils.default.stringifyRequest(this, `!!${request}`);
// result: "!!../../node_modules/css-loader/dist/cjs.js!./global.css"
複製代碼

因此 content 的實際內容就是:

var content = require("!!../../node_modules/css-loader/dist/cjs.js!./global.css");
複製代碼

也就是在這裏纔去調用 css-loader 來處理樣式文件。

!! 模塊前面的兩個感嘆號的做用是禁用 loader 的配置的,若是不由用的話會出現無限遞歸調用的狀況。

一樣的,update 的實際內容是:

var update = require("!../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js")(content, options);
複製代碼

意思也就是調用 injectStylesIntoStyleTage 模塊來處理通過 css-loader 處理過的樣式內容 content

上述代碼都是 style-loader 返回的,真正執行是在 runtime 階段。

runtime 階段

原本都寫好了,忽然不見了,心痛。

簡單地寫一下吧,具體的源碼見 傳送門

將樣式插入 DOM 的操做實際是在 runtime 階段進行的,仍是以默認狀況舉例,看看 injectStylesIntoStyleTage 作了什麼。

簡單來講,module.exports裏最主要的就是 insertStyleElementapplyToTag 兩個方法,簡化一下就是這樣的:

module.exports = (list, options) => {
  options = options || {};
  const styles = listToStyles(list, options);
  addStylesToDom(styles, options);
}

function insertStyleElement(options) {
  var style = document.createElement('style');
  
  Object.keys(options.attributes).forEach(function (key) {
    style.setAttribute(key, options.attributes[key]);
  });
  
  return style;
}

function applyToTag(style, options, obj) {
  var css = obj.css;
  var media = obj.media;

  if (media) {
    style.setAttribute('media', media);
  }

  if (style.styleSheet) {
    style.styleSheet.cssText = css;
  } else {
    while (style.firstChild) {
      style.removeChild(style.firstChild);
    }
    style.appendChild(document.createTextNode(css));
  }
}
複製代碼

和咱們上文猜想差很少是一致的,至此 style-loader 的主要工做就完成了。

相關文章
相關標籤/搜索