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.