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

什么是 style-loader

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 中

稍后会详细分析源码,看看和我们的思路是否一致。

如何使用 style-loader

1. 安装 style-loader

  •  
npm install style-loader --save-dev

2. 配置 webapck

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// webpack.config.jsmodule.exports = {  module: {    rules: [      {        test: /\.(css)$/,        use: [          {            loader: 'style-loader',            options: {},          },          { loader: 'css-loader' },        ],      },    ],  },};

日常的开发中处理样式文件时,一般会使用到 style-loader 和 css-loader 这两个 loader

关于 style-loader 的 options,这里就不多说了,见 style-loader options .

3. 引入一个样式文件

  •  
const indexStyle = require('./assets/style/index.css');

4. 见证奇迹的时刻

  •  
webpack

打包完成之后我们打开 html 页面,会看到 <head> 里已经有了 index.css 里的样式内容:

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

injectType

单独讲一下 injectType 这个配置项,默认值是 styleTag,通过 <style></style> 的形式插入 DOM 中,我们来看看不同的 injectType 的效果。

styleTag

默认情况下,style-loader 每一次处理引入的样式文件都会在 DOM 上创建一个 <style> 标签,比如此时引入两个样式文件:

  •  
  •  
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' },  ],}
// jsconst 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

当 injectType 为 linkTag,会通过 <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' },  ],}
// jsconst 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' },  ],}
// jsconst 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 配置里的 optionsconst options = _loaderUtils.default.getOptions(this) || {};// 校验 options(0, _schemaUtils.default)(_options.default, options, {  name: 'Style Loader',  baseDataPath: 'options'});
// style 标签插入的位置,默认是 headconst 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#injecttypeconst injectType = options.injectType || 'styleTag';
switch (injectType) {  case 'linkTag': {}  case 'lazyStyleTag':  case 'lazySingletonStyleTag': {}  case 'styleTag':  case 'singletonStyleTag':  default: {}}

根据不同的 injectType 会 return 不同的 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里最主要的就是 insertStyleElement 和 applyToTag 两个方法,简化一下就是这样的:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
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 的主要工作就完成了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值