[Инженерия] Объясните CSS-модули простым способом

JavaScript CSS
[Инженерия] Объясните CSS-модули простым способом

Что такое CSS-модули?

Официальная документация выглядит следующим образом:

A CSS Modules is a CSS file in which all class names and animation names are scoped locally by default.

Все имена классов и имена анимаций по умолчанию имеют свою область видимости.CSSдокумент. Модули CSS не являются ни официальным стандартом CSS, ни функцией браузера, но используют некоторые инструменты сборки, такие какwebpack,правильноCSSСпособ определения области имен классов и селекторов (аналогично пространствам имен)

Эта статья познакомитCSS Modulesпростота использования и CSS ModulesПринцип реализации (реализация в CSS-загрузчике)

Простое использование модулей CSS

Установка и настройка проекта

Создайте новый проект,Demo

npx create-react-app learn-css-modules-react
cd learn-css-modules-react
# 显示 webpack 的配置
yarn eject

Видетьconfig/webpack.config.js, по умолчанию проект, построенный с помощью скаффолдинга React, имеет только.module.cssПоддержка модульности, если вы строите ее самостоятельно, вы можете поддерживать суффикс файлов .css и т. д.

// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
  test: cssModuleRegex,
  use: getStyleLoaders({
    importLoaders: 1,
    sourceMap: isEnvProduction
      ? shouldUseSourceMap
      : isEnvDevelopment,
    modules: {
      getLocalIdent: getCSSModuleLocalIdent,
    },
  }),
}

Среди них функция getStyleLoaders, можно посмотреть конфигурацию css-loader

const getStyleLoaders = (cssOptions, preProcessor) => {
  const loaders = [
    // ...
    {
      loader: require.resolve('css-loader'),
      options: cssOptions,
    },
    // ...
  ];
  // ...
  return loaders;
};

Мы будем использовать эту среду в качестве примера для демонстрации

локальная область

предыдущий стиль

Во-первых, мы будемApp.cssизменился наApp.module.cssА потом импортировать css, и установить (Есть небольшое знание, на самом деле CSS Modules рекомендуется с именем camel, в основном это так, используйте объекты style.classNameможно получить доступ. Если это так, вам нужноstyles['***-***'])

import styles from './App.module.css';

// ...
<header className={styles['App-header']}></header>

Соответствующее имя класса будет сгенерировано в соответствии с определенными правилами.

Это правило именования можно настроить через загрузчик CSS, аналогично следующей конфигурации:

module: {
  loaders: [
    // ...
    {
      test: /\.css$/,
      loader: "style-loader!css-loader?modules&localIdentName=[path][name]---[local]---[hash:base64:5]"
    },
  ]
}

глобальная область

Мы обнаружили, что по умолчанию имена классов, определенные в модулях css, должны задаваться аналогично настройке переменных.HTMLНастройки (как в примере выше)

тогда я могу быть как другиеCSSИспользует ли файл имя класса напрямую (то есть обычный метод настройки) вместо скомпилированной хеш-строки?

использовать:globalсинтаксис, вы можете объявить глобальное правило

:global(.App-link) {
  color: #61dafb;
}

Таким образом, вы можете использовать обычный CSS прямо в HTML.

Но это похоже на бэкдор, оставленный CSS-модулями для разработчиков.. Лучше поместить CSS, как у нас, прямо в обычный файл .css. Я понимаю, что именно поэтому React по-разному обрабатывает разные суффиксы .css и .module.css. для обработки

Комбинации классов

В модулях CSS селектор может наследовать правила другого селектора, что называется «комбинацией»."composition")

Например, мы определяем font-red и используем его в .App-header.composes: font-red;наследовать

.font-red {
  color: red;
}

.App-header {
  composes: font-red;
  /* ... */
}

импортировать другие модули

Не только в одном файле, но и наследовать правила CSS в других файлах

определитьanother.module.css

.font-blue {
  color: blue;
}

в App.module.css

.App-header {
  /* ... */
  composes: font-blue from './another.module.css';
  /* ... */
}

использовать переменные

Мы также можем использовать переменные, определяяcolors.module.css

@value blue: #0c77f8;

в App.module.css

@value colors: "./colors.module.css";
@value blue from colors;

.App-header {
  /* ... */
  color: blue;
}

использовать сводку

В общем, использование CSS-модулей относительно простое, и начать работу с ним очень быстро.Далее давайте взглянем на Webpack.CSS-loader как это достигаетсяCSS Modulesиз

Принцип реализации модулей CSS

Начните с загрузчика CSS

Смотретьlib/processCss.jsсередина

var pipeline = postcss([
	...
	modulesValues,
	modulesScope({
    // 根据规则生成特定的名字
		generateScopedName: function(exportName) {
			return getLocalIdent(options.loaderContext, localIdentName, exportName, {
				regExp: localIdentRegExp,
				hashPrefix: query.hashPrefix || "",
				context: context
			});
		}
	}),
	parserPlugin(parserOptions)
]);

В основном см.modulesValuesа такжеmodulesScopeМетод, на самом деле эти два метода поступают из двух других пакетов

var modulesScope = require("postcss-modules-scope");
var modulesValues = require("postcss-modules-values");

postcss-modules-scope

Этот пакет в основном обеспечивает изоляцию стилей модулей CSS (Scope Local) и наследование (Extend).

Его код относительно прост, в основном файл завершен, исходный код можно увидетьздесь, здесь будет использоваться postcss для обработки AST, мы можем примерно понять его идею

соглашение об именах по умолчанию

На самом деле, если вы не установите никаких правил, он будет называться следующим образом

// 生成 Scoped name 的方法(没有传入的时候的默认规则)
processor.generateScopedName = function (exportedName, path) {
  var sanitisedPath = path.replace(/\.[^\.\/\\]+$/, '').replace(/[\W_]+/g, '_').replace(/^_|_$/g, '');
  return '_' + sanitisedPath + '__' + exportedName;
};

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

var processor = _postcss2['default'].plugin('postcss-modules-scope', function (options) {
  // ...
  return function (css) {
    // 如果有传入,则采用传入的命名规则
   	// 否则,采用默认定义的 processor.generateScopedName
    var generateScopedName = options && options.generateScopedName || processor.generateScopedName;
  }
  // ...
})

Pre-knowledge - метод обхода стилей postcss

css astЕсть в основном 3 родительских типа

  • AtRule: Этот тип @xxx, такой как @screen, потому что использование переменных будет упомянуто ниже.@value
  • Комментарий: комментарий
  • Правило: обычное правило css

Есть еще несколько важных подтипов:

  • decl: относится к каждому конкретному правилу css
  • правило: набор правил css, которые действуют на селектор

Различные типы выполняют разные обходы

  • walk: просмотреть всю информацию об узле, будь то родительский тип atRule, правило, комментарий илиrule,declподтип
  • walkAtRules: пройтись по всем atRules
  • walkComments: просмотр комментариев
  • walkDecls
  • walkRules

Реализация ограниченного стиля

// Find any :local classes
// 找到所有的含有 :local 的 classes
css.walkRules(function (rule) {
  var selector = _cssSelectorTokenizer2['default'].parse(rule.selector);
  // 获取 selector
  var newSelector = traverseNode(selector);
  rule.selector = _cssSelectorTokenizer2['default'].stringify(newSelector);
  // 遍历每一条规则,假如匹配到则将类名等转换成作用域名称
  rule.walkDecls(function (decl) {
    var tokens = decl.value.split(/(,|'[^']*'|"[^"]*")/);
    tokens = tokens.map(function (token, idx) {
      if (idx === 0 || tokens[idx - 1] === ',') {
        var localMatch = /^(\s*):local\s*\((.+?)\)/.exec(token);
        if (localMatch) {
          // 获取作用域名称
          return localMatch[1] + exportScopedName(localMatch[2]) + token.substr(localMatch[0].length);
        } else {
          return token;
        }
      } else {
        return token;
      }
    });
    decl.value = tokens.join('');
  });
});

css.walkRules просматривает всю информацию об узле, будь то родительский тип atRule, правило, комментарий илиrule,declподтип, получить селектор

// 递归遍历节点,找到目标节点
function traverseNode(node) {
  switch (node.type) {
    case 'nested-pseudo-class':
      if (node.name === 'local') {
        if (node.nodes.length !== 1) {
          throw new Error('Unexpected comma (",") in :local block');
        }
        return localizeNode(node.nodes[0]);
      }
      /* falls through */
    case 'selectors':
    case 'selector':
      var newNode = Object.create(node);
      newNode.nodes = node.nodes.map(traverseNode);
      return newNode;
  }
  return node;
}

walkDeclsПройдите каждое правило и сгенерируйте соответствующийScoped Name

// 生成一个 Scoped Name
function exportScopedName(name) {
  var scopedName = generateScopedName(name, css.source.input.from, css.source.input.css);
  exports[name] = exports[name] || [];
  if (exports[name].indexOf(scopedName) < 0) {
    exports[name].push(scopedName);
  }
  return scopedName;
}

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

postcss-modules-values

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

Его реализация представляет собой только один файл, конкретныйПосмотреть здесь

посмотреть все@valueоператоры и рассматривать их как локальные переменные или импортированные, и, наконец, сохранить вdefinitionsв объекте

/* Look at all the @value statements and treat them as locals or as imports */
// 查看所有的 @value 语句,并将它们视为局部变量还是导入的
css.walkAtRules('value', atRule => {
  // 类似如下的写法
  // @value primary, secondary from colors 
  if (matchImports.exec(atRule.params)) {
    addImport(atRule)
  } else {
    // 处理定义在文件中的 类似如下
    // @value primary: #BF4040;
		// @value secondary: #1F4F7F;
    if (atRule.params.indexOf('@value') !== -1) {
      result.warn('Invalid value definition: ' + atRule.params)
    }

    addDefinition(atRule)
  }
})

Если он импортирован, вызовите метод addImport.

const addImport = atRule => {
  // 如果有 import 的语法
  let matches = matchImports.exec(atRule.params)
  if (matches) {
    let [/*match*/, aliases, path] = matches
    // We can use constants for path names
    if (definitions[path]) path = definitions[path]
    let imports = aliases.replace(/^\(\s*([\s\S]+)\s*\)$/, '$1').split(/\s*,\s*/).map(alias => {
      let tokens = matchImport.exec(alias)
      if (tokens) {
        let [/*match*/, theirName, myName = theirName] = tokens
        let importedName = createImportedName(myName)
        definitions[myName] = importedName
        return { theirName, importedName }
      } else {
        throw new Error(`@import statement "${alias}" is invalid!`)
      }
    })
    // 最后会根据这个生成 import 的语法
    importAliases.push({ path, imports })
    atRule.remove()
  }
}

В противном случае добавьте определение напрямую.Я вообще понимаю, что две идеи - найти переменную ответа, а затем заменить ее

// 添加定义
const addDefinition = atRule => {
  let matches
  while (matches = matchValueDefinition.exec(atRule.params)) {
    let [/*match*/, key, value] = matches
    // Add to the definitions, knowing that values can refer to each other
    definitions[key] = replaceAll(definitions, value)
    atRule.remove()
  }
}

Суммировать

Модули CSS не являются ни официальным стандартом CSS, ни функцией браузера, а способом определения имен и селекторов классов CSS (аналогично пространствам имен) с использованием некоторых инструментов сборки, таких как webpack. С помощью модулей CSS мы можем реализовать локальную область действия CSS, комбинацию классов и другие функции. Наконец, мы знаем, что CSS Loader на самом деле реализован двумя библиотеками. в,postcss-modules-scope—— Реализовать изоляцию стилей CSS-модулей (Scope Local) и наследование (Extend) иpostcss-modules-values- Передавать произвольные значения между файлами модулей

Ссылаться на