Секрет принципа CSS TreeShaking: написание PurgeCss от руки

JavaScript PostCSS
Секрет принципа CSS TreeShaking: написание PurgeCss от руки

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

Для JS будем использовать Webpack и Terser для TreeShking, а для CSS будем использовать PurgeCss.

PurgeCss анализирует использование селекторов css в html или другом коде и удаляет неиспользуемые css.

Вам интересно, как PurgeCss находит бесполезный CSS? Сегодня мы напишем упрощенную версию PurgeCss, чтобы изучить ее.

Анализ мыслей

PurgeCss нужно указать, к какому html применяется css, он проанализирует селектор css в html и удалит неиспользуемый css в соответствии с результатом анализа:

const { PurgeCSS } = require('purgecss')
const purgeCSSResult = await new PurgeCSS().purge({
  content: ['**/*.html'],
  css: ['**/*.css']
})

То, что нам предстоит сделать, можно разделить на две части:

  • Извлеките возможные селекторы css в html, включая идентификатор, класс, тег и т. д.
  • Проанализируйте правила в css и удалите неиспользуемые части в зависимости от того, используется ли селектор в html.

Часть, которая извлекает информацию из html, называется экстрактором html.

Мы можем реализовать экстрактор html на основе posthtml, который может выполнять синтаксический анализ html, анализ, преобразование и т. д. API похож на postcss.

Часть css использует postcss, и каждое правило можно анализировать через ast.

Пройдитесь по правилам css, и оцените, извлекается ли селектор каждого правила из html в селектор, если нет, значит он не используется, а селектор удаляется.

Если все селекторы правила удалены, то удалите правило.

Это реализация идеи purgecss. Давайте напишем код.

Код

Для этого напишем плагин postcss, который анализирует и конвертирует css на основе AST.

const purgePlugin = (options) => {
  
    return {
        postcssPlugin: 'postcss-purge',
        Rule (rule) {}
    }
}

module.exports = purgePlugin;

Форма плагина postcss — это функция, которая получает параметры конфигурации плагина и возвращает объект. В объекте объявляются слушатели для Rule, AtRule, Decl и т. д., то есть функции обработки для разных AST.

Плагин postcss называется purge и может быть вызван следующим образом:

const postcss = require('postcss');
const purge = require('./src/index');
const fs = require('fs');
const path = require('path');
const css = fs.readFileSync('./example/index.css');

postcss([purge({
    html: path.resolve('./example/index.html'),
})]).process(css).then(result => {
    console.log(result.css);
});

Передайте путь html через параметры, которые можно получить через option.html в плагине.

Далее давайте реализуем этот плагин.

Как было проанализировано ранее, общий процесс внедрения делится на два этапа:

  • Извлечь идентификатор, класс, тег в html через posthtml
  • Пройдите через css и удалите часть, которая не используется html.

Мы инкапсулируем htmlExtractor для извлечения:

const purgePlugin = (options) => {
    const extractInfo = {
        id: [],
        class: [],
        tag: []
    };

    htmlExtractor(options && options.html, extractInfo);

    return {
        postcssPlugin: 'postcss-purge',
        Rule (rule) {}
    }
}

module.exports = purgePlugin;

Конкретная реализация htmlExtractor заключается в чтении содержимого html, анализе html для генерации AST, обходе AST и записи идентификатора, класса и тега:

function htmlExtractor(html, extractInfo) {
    const content = fs.readFileSync(html, 'utf-8');

    const extractPlugin = options => tree => {      
        return tree.walk(node => {
            extractInfo.tag.push(node.tag);
            if (node.attrs) {
              extractInfo.id.push(node.attrs.id)
              extractInfo.class.push(node.attrs.class)
            }
            return node
        });
    }

    posthtml([extractPlugin()]).process(content);

    // 过滤掉空值
    extractInfo.id = extractInfo.id.filter(Boolean);
    extractInfo.class = extractInfo.class.filter(Boolean);
    extractInfo.tag = extractInfo.tag.filter(Boolean);
}

Форма плагина posthtml аналогична postcss.Мы просматриваем AST и записываем некоторую информацию в плагин posthtml.

Наконец, отфильтруйте пустые значения в идентификаторе, классе и теге, чтобы завершить извлечение.

Не будем торопиться с переходом к следующему шагу, сначала протестируем текущую функцию.

Готовим такой html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="aaa"></div>

    <div id="ccc"></div>

    <span></span>
</body>
</html>

Информация, извлеченная в ходе теста:

Как видите, идентификатор, класс и тег правильно извлекаются из html.

Далее переходим к следующему шагу: удаляем неиспользуемые части из AST css.

Мы объявляем слушателя правила и можем получить AST правила. Чтобы проанализировать часть селектора, вам нужно сначала разбить ее по «,», а затем обработать каждый селектор.

Rule (rule) {                        
     const newSelector = rule.selector.split(',').map(item => {
        // 对每个选择器做转换
    }).filter(Boolean).join(',');

    if(newSelector === '') {
        rule.remove();
    } else {
        rule.selector = newSelector;
    }
}

Селекторы можно анализировать, анализировать и преобразовывать с помощью postcss-selector-parser.

Если все селекторы после обработки удаляются, значит стиль правила бесполезен, и правило удаляется. В противном случае некоторые селекторы могут быть просто удалены, а стиль все равно будет использоваться.

const newSelector = rule.selector.split(',').map(item => {
    const transformed = selectorParser(transformSelector).processSync(item);
    return transformed !== item ? '' : item;
}).filter(Boolean).join(',');

if(newSelector === '') {
    rule.remove();
} else {
    rule.selector = newSelector;
}

Далее реализуем анализ и преобразование селектора, то есть функцию transformSelector.

Логика этой части заключается в том, чтобы судить, есть ли каждый селектор в извлеченном из html селекторе, если нет, то удалить его.

const transformSelector = selectors => {
    selectors.walk(selector => {
        selector.nodes && selector.nodes.forEach(selectorNode => {
            let shouldRemove = false;
            switch(selectorNode.type) {
                case 'tag':
                    if (extractInfo.tag.indexOf(selectorNode.value) == -1) {
                        shouldRemove = true;
                    }
                    break;
                case 'class':
                    if (extractInfo.class.indexOf(selectorNode.value) == -1) {
                        shouldRemove = true;
                    }
                    break;
                case 'id':
                    if (extractInfo.id.indexOf(selectorNode.value) == -1) {
                        shouldRemove = true;
                    }
                    break;
            }

            if(shouldRemove) {
                selectorNode.remove();
            }
        });
    });
};

Мы завершили извлечение информации о селекторе в html и удаление бесполезных правил в css на основе информации, извлеченной из html, и функция плагина завершена.

Проверим эффект:

CSS:

.aaa, ee , ff{
    color: red;
    font-size: 12px;
}
.bbb {
    color: red;
    font-size: 12px;
}

#ccc {
    color: red;
    font-size: 12px;
}

#ddd {
    color: red;
    font-size: 12px;
}

p {
    color: red;
    font-size: 12px;
}
span {
    color: red;
    font-size: 12px;
}

html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="aaa"></div>

    <div id="ccc"></div>

    <span></span>
</body>
</html>

Само собой разумеется, что селекторы и стили p, #ddd, .bbb, а также селекторы ee и ff будут удалены.

Мы используем этот плагин:

const postcss = require('postcss');
const purge = require('./src/index');
const fs = require('fs');
const path = require('path');
const css = fs.readFileSync('./example/index.css');

postcss([purge({
    html: path.resolve('./example/index.html'),
})]).process(css).then(result => {
    console.log(result.css);
});

После тестирования функция верна:

Вот как реализован PurgeCss. Мы сделали три встряхивания css!

Код загружен на github:GitHub.com/кварк глюон P…

Конечно, мы реализуем только упрощенную версию, и некоторые места не идеальны:

  • Реализован только экстрактор html, а в PurgeCss есть еще такие экстракторы, как jsx, pug, tsx (но идея та же)
  • Обрабатывается только один файл, а не несколько файлов (просто добавьте цикл)
  • Обрабатываются только селекторы id, class, tag, а селекторы атрибутов не обрабатываются (обработка селекторов атрибутов немного сложнее)

Хотя это не идеально, идея реализации PurgeCss была передана, не так ли?

Суммировать

TreeShking JS использует Webpack, Terser, а TreeShking CSS использует PurgeCss.

Мы реализовали упрощенную версию PurgeCss, чтобы прояснить, как она работает:

Извлеките информацию о селекторе в html с помощью экстрактора html, а затем отфильтруйте CSS AST и удалите неиспользуемые правила в зависимости от того, используется ли селектор правила для достижения цели TreeShking.

В процессе реализации этого инструмента мы научились писать плагины postcss и posthtml, которые похожи по форме, за исключением того, что один для анализа и преобразования css, а другой для html.