- PVSM.RU - https://www.pvsm.ru -
Я считаю, что CSS Модули — это монументальный проект. С его помощью можно решить одну худших проблем CSS — коллизию имен классов. Давайте рассмотрим простой пример, чтобы было понятно, о чем идет речь.
Представим, что мы разрабатываем компонент Button. Использовать "чистый" CSS опасно, потому что есть риск, что кто-то ещё в вашем проекте (или ещё хуже — в подключенной библиотеке) использует то же имя класса:
/* Button.css */
.button {
color: #f00;
padding: 10px;
font-size: 18px;
}
/* node_modules/some_lib/styles.css */
.button {
color: #0f0;
}
// Button.tsx
import { FC } from "react";
import "./Button.module.css";
import "some_lib/styles.css";
export const Button: FC = (props) => {
// Какого цвета будет кнопка остаётся только гадать
return <button {...props} className="button" />;
};
CSS Modules решают эту проблему достаточно изящно. Модули хешируют все имена классов в файле и генерируют мап типа такого:
export default {
button: '_2bs4j'
}
Разработчик импортирует этот объект к себе и обращается к его ключам:
// Button.tsx
import { FC } from "react";
import styles from "./Button.module.css";
import "some_lib/styles.css";
export const Button: FC = (props) => {
// Теперь мы можем не беспокоиться о конфликте селекторов, так как наш селектор
// на этапе билда превратится в "_2bs4j"
return <button {...props} className={styles.button} />;
};
Если вы используете Webpack, то включить поддержку CSS Modules можно практически не трогая конфиг:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.module.css$/,
use: [
{
loader: "css-loader",
options: {
modules: true, // Раз — и готово
},
}
],
}
],
},
};
Если на проекте вы используете TypeScript (я очень на это надеюсь), при подключении CSS Modules вы скорее всего столкнетесь с проблемой, так как TS не знает, что делать с CSS файлами. Компилятору неоткуда брать типизацию для них. Но проблема достаточно легко решается написанием простенькой декларации:
// global.d.ts
declare module "*.css" {
export default {
[index: string]: string;
}
}
Переводя на человеческий, мы говорим, что любые файлы с расширением .css генерируют строковый мап. TS доволен. Но теперь есть другая проблема — он вообще всем доволен :D Я про то, что вы можете обращаться к несуществующему имени класса и TS вас об этом не предупредит:
// Button.tsx
import { FC } from "react";
import styles from "./Button.module.css";
export const Button: FC = (props) => {
// По ошибке пропустили "t" в слове "button"
return <button {...props} className={styles.buton} />;
};
Я думаю, нет необходимости рассуждать на тему того, почему это плохо. Давайте попробуем исправить ситуацию.
К счастью, сообщество уже думало над этой проблемой: typescript-plugin-css-modules [1]. Данный плагин создаёт виртуальный .d.ts для каждого CSS файла и таким образом помогает IDE находить возможные баги:
// tsconfig.json
{
"compilerOptions": {
"plugins": [
{
"name": "typescript-plugin-css-modules"
}
]
}
}
// Button.tsx
import { FC } from "react";
import styles from "./Button.module.css";
export const Button: FC = (props) => {
// Теперь IDE скажет, что тут мы обращаемся к несуществующему полю "buton"
return <button {...props} className={styles.buton} />;
};
Вроде бы круто, мы добились чего хотели. Но если запустить TypeScript через CLI, то внезапно никакой ошибки показано не будет. Это происходит потому что TypeScript не очень поддерживает [2] эти плагины. Более того, Webpack нас об опечатке тоже не уведомит. С его точки зрения код корректен. То есть проблему мы решили лишь частично. Риск того, что подобный код попадет на прод всё ещё велик.
Перед тем как решать вышеописанную проблему, я предлагаю вспомнить кое что из основ JS. Поговорим о ES Modules. Этот стандарт предоставляет два типа экспорта (и импорта): именованный и дефолтный. В своих проектах я предпочитаю использовать именованный экспорт, потому считаю, что он делает код более надежным. Давайте рассмотрим два примера:
// module1.ts
export const foo = "foo";
export const bar = "bar";
// module2.ts
import { foo, bar } from "./module1";
console.log(foo); // "foo"
// module1.ts
export default { foo: "foo", bar: "bar" };
// module2.ts
import module1 from "./module1";
console.log(module1.foo) // "foo"
Примеры похожие, но между ними есть существенная разница. Если вы используете именованный экспорт, инструменты вроде Webpack кинут ошибку при попытке импортировать то, что не экспортируется:
// module1.ts
export const foo = "foo";
export const bar = "bar";
// module2.ts
import { foo, bar, baz } from "./module1";
// ERROR: export 'baz' (imported as 'module1') was not found in './module1.ts' (possible exports: foo, bar)
Дефолтный экспорт работает иначе. Например, если вы экспортируете объект в качестве дефолта и пытаетесь получить доступ к свойству, которого не существует, бандлер не скажет, что что-то идет не так:
// module1.ts
export default { foo: "foo", bar: "bar" };
// module2.ts
import module1 from "./module1";
console.log(module1.baz) // undefined
То есть о том, что вы обращаетесь к несуществующему полю вы узнаете в рантайме, а не во время билда.
Итак, как я понимаю, нам нужно экспортировать мапу с классами, используя именованные экспорты вместо дефолтных. Такой апдейт поможет нам отлавливать ошибки на этапе сборки проекта. Конфиг Webpack нужно будет обновить примерно так:
// webpack.config.js
module.exports = {
module: {
strictExportPresence: true, // Включаем строгий режим, чтобы попытка импортировать несуществующие объекты приводила к падению билда
rules: [{
test: /.module.css$/,
use: ["css-loader", {
options: {
esModule: true, // Говорим о том, что хотим использовать ES Modules
modules: {
namedExport: true, // Указываем, что предпочитаем именованый экспорт дефолтному
},
}
}]
}]
}
};
Теперь подобный код упадет с ошибкой при попытке его собрать:
import styles from "./Button.module.css"; // Дефолтного экспорта больше нет
Сборка такого кода тоже рухнет, так как в импорте допущена опечатка:
import { buton } from "./Button.module.css";
С новой конфигурацией Вебпака использование стилей будет выглядеть так:
import { button } from "./Button.module.css";
console.log(button) // "_2bs4j"
Или так (так даже удобнее, чтобы не плодить кучу локальных констант):
import * as styles from "./Button.module.css";
console.log(styles.button) // "_2bs4j"
Кстати, styles.buton тоже приведет к падению билда. Напомню, что при дефолтном импорте такой код был воспринят сборщиком как корректный.
Отлично. Теперь мы можем быть уверены в нашем коде. Он не попадет на прод в случае, если в нем мы обращаемся к несуществующим классам. Мне нравится это решение ещё и тем, что оно работает даже в проектах без TS.
Плагин TS всё ещё думает, что стили экспортируются по-дефолту. Это не прикольно, потому что IDE не предупредит нас о том, что мы делаем что-то не так. Да, билд упадет, но мы же не хотим всякий раз смотреть в консоль, когда что-то делаем с кодом? Будет здорово уже на этапе печатания получить информацию о том, что в коде появился баг.
Согласно доке [3], плагин всегда генерирует типизацию дефолтного экспорта. Грустно. Но у нас есть возможность изменить это поведение. Для этого воспользуемся опцией customTemplate. В качестве значения пропишем путь к модулю с функцией, задача которой будет принимать мап с css классами и выдавать его типизацию:
// tsconfig.json
{
"compilerOptions": {
"plugins": [
{
"name": "typescript-plugin-css-modules",
"options": {
"customTemplate": "./customTemplate.js"
}
}
]
}
}
// customTemplate.js
module.exports = (dts, { classes }) => {
return Object.keys(classes)
.map((key) => `export const ${key}: string`)
.join("n");
};
Всё, в .d.ts файлах больше не будет export default .
Теперь и IDE, и бандлер будут предостерегать нас от использования несуществующих CSS классов.
Webpack, TypeScript и CSS позволяют писать изолированные строго типизированные стили. Я надеюсь, эта статья поможет вам улучшить надежность вашей кодовой базы. Спасибо за прочтение :)
Автор: Артур Стамбульцян
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/css/378979
Ссылки в тексте:
[1] typescript-plugin-css-modules: https://www.npmjs.com/package/typescript-plugin-css-modules
[2] не очень поддерживает: https://github.com/microsoft/TypeScript/issues/16607
[3] доке: https://www.npmjs.com/package/typescript-plugin-css-modules#importing-css
[4] Репозиторий с полным примером: https://github.com/ArthurStam/typed-css-example
[5] Источник: https://habr.com/ru/post/688844/?utm_source=habrahabr&utm_medium=rss&utm_campaign=688844
Нажмите здесь для печати.