В процессе разработки JS модулей, претендующих на переиспользуемость, часто возникает необходимость кроме js-кода и разметки подгрузить еще и файл со стилями. Как известно, сами по себе стили могут быть добавлены к документу тремя способами: через тэг link, через тэг style, и через атрибут style. В зависимости от выбранного способа можно получить различные плюсы и минусы. Я предлагаю посмотреть на способ, состоящий из использования тэга link, но избавленный от проблемы отсутствия события окончания загрузки таблицы стилей со стороны браузера.

для тех кому только код
define(["text!myCSS.css"], function(cssText) {
	'use strict';
	var link = document.createElement("link");
	link.rel = "stylesheet";
	link.type = "text/css"
	link.href = "data:text/css,"+encodeURI(cssText);
	var cssLinks = document.querySelectorAll("link[rel=stylesheet]");
	
	if (cssLinks.length > 0) {
		cssLinks[0].parentElement.insertBefore(link, cssLinks[0]);
	} else {
		document.querySelector("head").appendChild(link);
	}
});


Начнем с ответа на вопрос «Зачем именно через тэг link?»

Дело в том, что от модуля обычно хочется получить возможность удобно кастомизировать его визуальное представление.
Если мы захотим использовать атрибут style прямо в нашей разметке, то в дальнейшем при переиспользовании нам придется как-то эти атрибуты менять — супер неудобно и не гибко.

Если мы захотим использовать тэг style, то возникает проблема с тем, что для кастомизации таких стилей придется также вставлять в документ тэг style с нужным содержимым, что также не очень удобно.

Если мы захотим использовать тэг link, то мы сможем просто подключить нашу собственную таблицу стилей после загруженной и подправить представление по-умолчанию под сиюминутные нужды.

Из вышеперечисленного выходит, что вариант с тэгом link наиболее гибкий, т.к. позволяет переопределить стили при использовании компонента наибольшим числом способов.

Как написано в документации RequireJS, для подгрузки css можно использовать такой код:

function loadCss(url) {
    var link = document.createElement("link");
    link.type = "text/css";
    link.rel = "stylesheet";
    link.href = url;
    document.getElementsByTagName("head")[0].appendChild(link);
}

Он всем неплох, кроме того что вы не узнаете, когда именно будет подгружен ваш css (и будет ли вообще, могли же, например, в путях опечататься). Эта проблема чревата еще и тем, что может вызвать некрасивое поведение картинки, в случае если рендер страницы будет проводится до окончания загрузки файла.

А хочется написать компонент примерно так:

define(["text!myCSS.css", "text!myHTML.html"], function(cssText, htmlText) {
	'use strict';
...
});

Но как же потом запихнуть полученный текст css в нашу страницу? Тэг style мог бы нам помочь, но ранее было решено, что наш вариант — это именно link.

Предлагаемое решение — это Data URL (если кто не знает, что это такое, здесь можно посмотреть).

Добавлять будем примерно так:

	var link = document.createElement("link");
	link.rel = "stylesheet";
	link.type = "text/css"
	link.href = "data:text/css,"+encodeURI(cssText);
	var cssLinks = document.querySelectorAll("link[rel=stylesheet]");
	
	if (cssLinks.length > 0) {
		cssLinks[0].parentElement.insertBefore(link, cssLinks[0]);
	} else {
		document.querySelector("head").appendChild(link);
	}

Метод несомненно имеет некоторые минусы, из очевидных — base64 кодирование увеличивает объем данных, теоретически можно столкнуться с ограничениями на длину URL для некоторых браузеров. Но если объем стилей небольшой, то метод выглядит вполне рабочим.

UPD:
Cпасибо oledje за подсказку, base64 кодирование для текста не нужно, лучше подойдет URL encoding, подправил код.
Поделиться с друзьями
-->

Комментарии (28)


  1. ArthurSupertramp
    04.08.2016 12:12
    +7

    Webpack передавал привет


    1. GerrAlt
      04.08.2016 12:28
      -1

      вы имеете ввиду что Webpack лучше чем RequireJS, или что Webpack умеет аккуратно подгружать css?


      1. bromzh
        04.08.2016 12:37
        +1

        И первое, и второе.


        1. stas404
          04.08.2016 12:51

          И компот «Hot Module Replacement»


        1. GerrAlt
          04.08.2016 15:09

          а не подскажите каким образом он подгружает стили и встраивает их в страницу? я с webpack дела не имел никогда


          1. ArthurSupertramp
            04.08.2016 17:23

            он либо прямо в сборку пихает стили в виде строки, либо собирает их в отдельный css, если попросить конфиги нужные прописать.


          1. bromzh
            04.08.2016 17:46
            +1

            Посмотрите скринкасты по вебпаку. Человек объясняет как пользоваться вебпаком очень доходчиво, по делу и без воды. Тему загрузки css тоже затрагивает.


            1. GerrAlt
              04.08.2016 19:25
              +2

              Спасибо за ссылку, выборочно посмотрел (вступление, и про css). Честно говоря не понравилось: если в двух словах то для моих задач выглядит огромным оверкиллом. Я бы даже рискнул сказать что судя по тому что я успел посмотреть RequireJS и Webpack немного для разного нужны. Мне кажется было бы корректнее сравнивать r.js и Webpack.

              В общем после поверхностного знакомства осталось ощущения как от сравнения подходов написания инструментов под *nix и Windows, в одном случае минималистичность и «единственное предназначение», а в другом «умеем все» но как-то неаккуратно, исходники даже без минимайзера после упаковки выглядят совсем не так как проект при разработке, какие-то свои сложные внутренние абстракции типа цепочек загрузчиков с pitch функциями и т.д.

              P.s. остался вопрос, а если у меня в названии какого-то файла в модуле или каталога присутствует символ "!" или даже 2 идущих подряд, то сборка не сломается?


              1. faiwer
                05.08.2016 17:05

                если в двух словах то для моих задач выглядит огромным оверкиллом

                В своих задачах вы уже дошли до, гхм, необычных потребностей, коли уж внедряете CSS из AMD. Следующий шаг Webpack :)


      1. Veikedo
        04.08.2016 12:55
        +1

        Серьёзно. Мучался с requirejs и бандлингом долгое время. На вебпак не хотел переходить, потому что думал, что он сложен и непонятен.

        В итоге всё же добрался до него и всё завелось за 10 минут.


    1. Delphinum
      04.08.2016 14:27
      -1

      А разве WebPack умеет динамически подгружать стили?


      1. Veikedo
        04.08.2016 14:39
        +1

        1. Delphinum
          04.08.2016 14:43

          Похоже на то. Спасибо.


    1. enepomnyaschih
      05.08.2016 17:02

      Пока этот баг открыт, к WebPack я не вернусь. Отладка превращается в ад. RequireJS — единственный подход к сборке проекта, не опирающийся на SourceMaps (ну, если не считать моего собственного сборщика).


      1. zorro1211
        07.08.2016 17:00

        It's now enabled by default in Canary and you can't configure it anymore.

        Можно уже через Canary пользоваться нормально. В Chrome тоже можно включить эту фичу, почитайте комменты в вашей ссылке.


    1. enepomnyaschih
      05.08.2016 17:12

      Кстати, а вот и баг в самом WebPack, который изрядно доставляет проблем при подключении CSS. Единственное найденное мной решение — создание промежуточного файла all.css, импортирующего внутрь себя все остальные CSS-файлы в правильном порядке. Как видно по дате, разработчики не торопятся исправлять.


  1. serginho
    04.08.2016 13:05
    +1

    Require-css ?


    define(['css!styles/main'], function() {
      //code that requires the stylesheet: styles/main.css
    });


    1. GerrAlt
      04.08.2016 14:57

      есть нюанс, вы видели как именно он подгружает стили?


    1. GerrAlt
      04.08.2016 15:15

      взято из кода плагина по вашей ссылке:

        var linkLoad = function(url, callback) {
          var link = document.createElement('link');
          link.type = 'text/css';
          link.rel = 'stylesheet';
          if (useOnload)
            link.onload = function() {
              link.onload = function() {};
              // for style dimensions queries, a short delay can still be necessary
              setTimeout(callback, 7);
            }
          else
            var loadInterval = setInterval(function() {
              for (var i = 0; i < document.styleSheets.length; i++) {
                var sheet = document.styleSheets[i];
                if (sheet.href == link.href) {
                  clearInterval(loadInterval);
                  return callback();
                }
              }
            }, 10);
          link.href = url;
          head.appendChild(link);
      }
      


      т.е. если мы хотим получить именно link, то он подставляет url в тэг и тем или иным способом проверяет что загрузка завершена, вопрос в том, что произойдет если я минифицировал и у паковал исходники, и указанный файл уже на самом деле изначально выгружен браузеру


      1. serginho
        04.08.2016 18:07

        Четно говоря, сам не проверял, но, судя по документации, этот модуль нормально работает с оптимизатором
        https://github.com/guybedford/require-css#optimizer-configuration


        1. GerrAlt
          04.08.2016 19:40

          вроде бы да, но загрузочная функция вызывается так:

            cssAPI.load = function(cssId, req, load, config) {
          
              (useImportLoad ? importLoad : linkLoad)(req.toUrl(cssId + '.css'), load);
          
          }
          


          т.е. в url всегда оказывается именно адрес css файла, а значит
              var link = document.createElement('link');
              link.type = 'text/css';
              link.rel = 'stylesheet';
              ...
              link.href = url;
              head.appendChild(link);
          

          будет именно вставлять элемент с обычным URL, а не DataURL как в моем случае. Я не вижу варианта как при исполнении такого кода браузер смог бы обойтись без отправки запроса на получение указанного css файла (или обращения к кэшу, что в данном случае не играет роли — положить в кэш браузера содержимое css-файла из JS кода мне не представляется выполнимым)


  1. oledje
    05.08.2016 00:45
    +1

    Так и не понял в чем преимущество вашего способа по сравнению с Require-css как упомянул serginho выше. Для чего тратить ресурсы на кодировку в base64 всего листинга ccs файла и забивать DOM DataUtl'ом?

    К тому же есть известная проблема с функцией `btoa` которая отказывается кодировать символы не входящие в Latin1. Попробуйте выполнить этот код:

    btoa("кириллица");
    

    Например с https://habracdn.net/habr/styles/1470318147/_build/global_main.css btoa работать отказывается. В целом это выглядит как будто вы поленились найти готовый инструмент и сделали свой велосипед. Или я чего-то не доглядел?


    1. GerrAlt
      05.08.2016 10:19
      +1

      Я бы не стал говорить о преимуществе данного метода перед Require-css, это просто интересный (как мне показалось) способ включения css стиля в страницу. Если все же сравнивать два метода, то в ветке выше я высказал предположение, что при определенных настройках плагина будут проблемы в модуле, после обработки оптимизатором. С другой стороны у описанного в статье метода проблемы из-за base64 вцелом (как минимум избыточная трата ресурсов на перекодирование), и в реализации функции-кодировщика в частности.

      Для постоянного применения я бы данный метод советовать не стал, но в определенных условиях способ может иметь преимущества (например в простоте: реализация занимает меньше 15 строчек, а ради какого-то модуля в проекте и css-ки из 2-х строк может незахотеться тащить плагин)


    1. GerrAlt
      05.08.2016 10:52
      +1

      Вы навели меня на интересную мысль: зачем вообще base64, если css это не бинарный формат а текстовый? В соответствии со спецификацией dataURL можно написать так:

      link.href = "data:text/css,"+encodeURI(cssText);
      


      Затраты на кодирование это не отменяет, но проблемы с символами вне Latin1 должно решить.


  1. spmbt
    05.08.2016 02:08
    +2

    Не нашёл ответа в статье о том, почему не рассматриваете загрузку через JS (document.styleSheets, insertRule, addRule)?
    Ведь она соответствует требованиям модульности, имеет максимальную гибкость (можно выбрать место расположения правил (приоритет их), а не делать appendChild к группе уже подгруженных правил). Загрузка текстов полностью контролируется в Require или подобными механизмами, если файл не смог загрузиться.

    Понятно, что тут написать надо побольше кода, но будем иметь самую суть управления стилями.

    (Более того, меняя rules, получаем нетипичную гибкость манипулирования страницей в реалтайме, но этим в библиотеках и просто в приложениях никогда не пользуются, рассматривая правила как что-то статичное. Но это — другая тема.)


    1. GerrAlt
      05.08.2016 10:34

      В статье по вашей ссылке для создания новой таблицы стилей, куда потом складываются правила, используется тэг style. Я бы хотел этого избежать, т.к. хочется задавать наиболее общие стили, которые при необходимости можно было наибольшим числом способов переопределить/изменить.


      1. justboris
        05.08.2016 12:26
        +1

        а разве есть разница в приоритете между стилями в теге <style /> и <link />?

        Мой опыт говорит, что они равнозначны, выигрывают те, кто идет последним в документе.


        1. GerrAlt
          05.08.2016 12:53
          +1

          похоже что вы правы, я пчм-то был уверен что встроенные таблицы стилей имеют преимущество при рендеренге перед внешними, но простейший тест

          <html>
              <head>
                  <style>
                      body {background-color: black;}
                  </style>
                  <link rel="stylesheet" href="data:text/css,body%20%7Bbackground-color:%20red%7D">
              </head>
              <body>
              </body>
          </html>
          

          показывает обратное: body красный.