Еще один способ подгрузки CSS через RequireJS

В процессе разработки 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, подправил код.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

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

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

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

          0
          И компот «Hot Module Replacement»
            0
            а не подскажите каким образом он подгружает стили и встраивает их в страницу? я с webpack дела не имел никогда
              0
              он либо прямо в сборку пихает стили в виде строки, либо собирает их в отдельный css, если попросить конфиги нужные прописать.
                +1

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

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

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

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

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

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

              В итоге всё же добрался до него и всё завелось за 10 минут.
              –1
              А разве WebPack умеет динамически подгружать стили?
              0
              Пока этот баг открыт, к WebPack я не вернусь. Отладка превращается в ад. RequireJS — единственный подход к сборке проекта, не опирающийся на SourceMaps (ну, если не считать моего собственного сборщика).
                0
                It's now enabled by default in Canary and you can't configure it anymore.

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

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

                Require-css ?


                define(['css!styles/main'], function() {
                  //code that requires the stylesheet: styles/main.css
                });
                  0
                  есть нюанс, вы видели как именно он подгружает стили?
                    0
                    взято из кода плагина по вашей ссылке:
                      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 в тэг и тем или иным способом проверяет что загрузка завершена, вопрос в том, что произойдет если я минифицировал и у паковал исходники, и указанный файл уже на самом деле изначально выгружен браузеру
                      0

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

                        0
                        вроде бы да, но загрузочная функция вызывается так:
                          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
                    Так и не понял в чем преимущество вашего способа по сравнению с Require-css как упомянул serginho выше. Для чего тратить ресурсы на кодировку в base64 всего листинга ccs файла и забивать DOM DataUtl'ом?

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

                    Например с https://habracdn.net/habr/styles/1470318147/_build/global_main.css btoa работать отказывается. В целом это выглядит как будто вы поленились найти готовый инструмент и сделали свой велосипед. Или я чего-то не доглядел?
                      +1
                      Я бы не стал говорить о преимуществе данного метода перед Require-css, это просто интересный (как мне показалось) способ включения css стиля в страницу. Если все же сравнивать два метода, то в ветке выше я высказал предположение, что при определенных настройках плагина будут проблемы в модуле, после обработки оптимизатором. С другой стороны у описанного в статье метода проблемы из-за base64 вцелом (как минимум избыточная трата ресурсов на перекодирование), и в реализации функции-кодировщика в частности.

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

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


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

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

                        (Более того, меняя rules, получаем нетипичную гибкость манипулирования страницей в реалтайме, но этим в библиотеках и просто в приложениях никогда не пользуются, рассматривая правила как что-то статичное. Но это — другая тема.)
                          0
                          В статье по вашей ссылке для создания новой таблицы стилей, куда потом складываются правила, используется тэг style. Я бы хотел этого избежать, т.к. хочется задавать наиболее общие стили, которые при необходимости можно было наибольшим числом способов переопределить/изменить.
                            +1
                            а разве есть разница в приоритете между стилями в теге <style /> и <link />?

                            Мой опыт говорит, что они равнозначны, выигрывают те, кто идет последним в документе.
                              +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 красный.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое