Это продолжение части 5 и является частью серии Путешествие по веб-приложениям. В этой статье мы представляем некоторые фактические макеты и стили с использованием CSS-модулей, гарантируя, что мы по-прежнему сможем отображать начальный ответ на сервере и последующую работу на клиенте (т. Е. Универсальный рендеринг).

Случай для модулей CSS

CSS является глобальным в приложении - классы CSS объявлены глобально и могут использоваться или заменяться более или менее в любом месте веб-приложения. Существует множество соглашений (например, БЭМ), чтобы избежать конфликтов имен между CSS для одного компонента и для другого.

Модули CSS - это альтернативный подход, разработанный для использования с компонентно-ориентированными библиотеками представлений (например, React), который, проще говоря, предполагает, что CSS для конкретного компонента по умолчанию должен быть доступен только локально для этого компонента. Для дальнейшего изучения изучите несколько полезных примеров и оригинальную статью.

В отличие от решений, ориентированных на JavaScript, модули CSS позволяют в полной мере использовать CSS (и, при необходимости, поддерживать автоматический префикс), но с улучшенным поведением для полнофункциональных веб-приложений. (Мы, вероятно, рассмотрим PostCSS и проект CCSNext в более поздней части этой серии.)

Модули CSS и Webpack

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

import React from 'react'
import styles from './Header.css'
export default class Header extends React.Component {
  render() {
    return (
      <header>
        <h1 className={styles.title}>A Web Application Journey</h1>
      </header>
    )
  }
}

Обратите внимание, как мы используем className вместо class (это требование для React JSX) и вместо установки атрибута на title мы используем выражение {styles.title} - это магия, необходимая для использования модулей CSS. . Styles будет объектом JavaScript, который представляет собой просто карту между локальным именем (в данном случае title) и некоторым глобально уникальным именем.

Наш Header.css прост:

.title {
    font-style: italic;
    font-size: 24pt;
}

Если мы попытаемся запустить наш проект в данный момент, Webpack выдаст ошибку, например:

ERROR in ./js/ui/Header.css
Module parse failed: /Users/phil/work/webappjourney/src/js/ui/Header.css Unexpected token (1:0)
You may need an appropriate loader to handle this file type.
| .title {
|     font-style: italic;
|     font-size: 24pt;
 @ ./js/ui/Header.js 11:0-34
...

Это напоминает нам, что нам нужно представить загрузчик Webpack для работы с этим типом файлов. Загрузчики Webpack перехватывают операторы загрузки зависимостей (например, ES2015 import или старый CommonJS require). В случае с Webpack, css-loader знает о стандарте CSS Modules, если мы включим флаг modules. Нам также понадобится другой загрузчик, который каким-то образом работает с собственно CSS и экспортирует сопоставление классов local: global. У нас есть два основных варианта:

  • Style-loader: распакует CSS и вставит в ‹head›
  • Extract-text-webpack-plugin (и соответствующая ему оболочка загрузчика): извлечет CSS и сгенерирует отдельный выходной файл CSS (для параллельной загрузки, развертывания через CDN и т. Д.)

Пока мы будем использовать последний, поэтому внесем следующие поправки в webpack.config.js. Вы заметите, что мы указываем конкретную схему сопоставления для имен наших классов; это будет преобразовано, например, в «Header_title__1Zmri» - это помогает при отладке, хотя хеша достаточно для производства / минификации.

const ExtractTextPlugin = require("extract-text-webpack-plugin")
// ... in the actual exported function
loaders: [
        // ... (e.g. babel loader)
        {
          test: /\.css$/, 
          loader: ExtractTextPlugin.extract({
            // fallbackLoader: "style-loader",
            loader: "css-loader?modules&localIdentName=[name]_[local]__[hash:base64:5]"
          })
        }
      ],
plugins: [
      new ExtractTextPlugin("styles.css")
    ]

Очевидно, нам нужно будет установить через npm (возможно, потребуется указать версии), то есть:

npm i -D css-loader extract-text-webpack-plugin

Теперь, если мы попытаемся собрать и запустить приложение на стороне сервера, этап компиляции будет работать, а запуск сервера - нет. (Примечание: «npm run go» - это сокращение, и обратите внимание, что мы переместили src / js / * только в src / *.)

> node dist-server/server.js
module.js:442
    throw err;
    ^
Error: Cannot find module './Header.css'
    at Function.Module._resolveFilename (module.js:440:15)
    at Function.Module._load (module.js:388:25)
    at Module.require (module.js:468:17)
    at require (internal/module.js:20:19)

Обработка зависимостей CSS на сервере

В dist-server нет файла Header.css, и даже если бы он был, node не знал бы, что с ним делать, поскольку это CSS, а не JavaScript. Помните, что мы не используем webpack для создания сервера (по крайней мере, пока). Давайте сначала воспользуемся приятной функцией интерфейса командной строки Babel для копирования некомпилируемых файлов (например, CSS) в package.json:

"build:server": "babel src --presets es2015-node,react --out-dir dist-server --copy-files"

И, на самом деле, у node действительно возникает проблема при попытке запустить:

> node dist-server/server.js
/Users/phil/work/webappjourney/dist-server/ui/Header.css:1
(function (exports, require, module, __filename, __dirname) { .title {
                                                              ^
SyntaxError: Unexpected token .

Это общая проблема с использованием Webpack в универсальной настройке: вся магия, для которой используется Webpack, не встроена в JavaScript / узел, поэтому нам нужно решение. Существует множество подходов, которые были разработаны или разрабатываются, в том числе:

  • Webpack-isomorphic-tools - раннее решение, но громоздкое в настройке
  • Universal-webpack - еще один подход автора webpack-isomorphic-tools, все еще немного громоздкий в настройке
  • Babel-plugin-webpack-loaders - новый плагин, который позволяет Babel запускать webpack (и его загрузчики) на стороне сервера.

В нашем случае, однако, мы будем использовать css-modules-require-hook, который существует уже некоторое время и достаточно прост в использовании. Он расширяет требование CommonJS (к которому импорт ES6 передается через Babel для узла) для работы с файлами CSS, возвращая объект JavaScript local: global map.

Давайте сначала установим ловушку (для производственной среды выполнения, а не просто для разработки):

npm i -S css-modules-require-hook

Теперь, когда мы расширяем базовую функциональность «require», ее необходимо загрузить до, когда мы начнем загружать какие-либо файлы CSS. Звучит банально; можно ожидать, что в начале server.js будет работать следующее:

const hook = require('css-modules-require-hook')
hook({
  generateScopedName: '[name]_[local]__[hash:base64:5]',
})

Однако это не совсем работает, поскольку в server.js есть операторы «import» из ES2015, и Babel переносит их в CommonJS / node require операторы в верхней части файла, перед вызовом функции hook (). Следовательно, благодаря импорту «маршрутов» мы зависим от файлов CSS перед вызовом ловушки.

Самое простое решение - ввести файл boostrapServer.js, который вводит ловушку, а затем загружает настоящий файл server.js. Избегая любых операторов импорта ES2015, Babel сохраняет последовательность.

Но мы еще не закончили! Когда мы запускаем это, похоже, что он работает (с учетом вспышки нестилизованного содержимого (FOUC), но проверка журнала консоли браузера обнаруживает полезное сообщение:

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
 (client) ass="Header_title__1Zmri" data-reactid="
 (server) ass="Header_title__1XDfy" data-reactid="

Хеш-код различается между тем, что css-loader Webpack генерирует для клиентского файла bundle.js, и тем, что генерирует css-modules-register-hook. Причина в том, что хеш основан на пути к файлам, и две библиотеки работают по разным путям (поскольку Babel транслируется в ./dist-server для сервера).

К счастью, в css-modules-register-hook есть опция, решающая эту проблему, поэтому необходимый код в bootstrapServer.js:

const path = require('path')
const hook = require('css-modules-require-hook')
const projectDir = path.resolve(__dirname)
hook({
  generateScopedName: '[name]_[local]__[hash:base64:5]',
  rootDir: projectDir
})
// We need to register the hook above first since ES2015 imports
// occur *before* statements are evaluated 
require('./server')

Конечно, нам нужно обновить наш серверный скрипт до следующего:

"start:server": "node dist-server/bootstrapServer.js",

Но тогда мы наконец готовы (см. Github)!

Вот файл styles.css, созданный сервером:

.Header_title__1Zmri {
    font-style: italic;
    font-size: 24pt;
}

И, для полноты, сгенерированный сервером HTML-ответ, в котором мы можем увидеть контрольные суммы, данные, обработанные повторно, и использование уникальных (сопоставленных) имен классов.

<html data-reactroot="" data-reactid="1" data-react-checksum="-186443981">
<head data-reactid="2">
  <title data-reactid="3">A Web Application Journey</title>
  <link rel="stylesheet" type="text/css" href="/static/styles.css" data-reactid="4" />
</head>
<body data-reactid="5">
  <div id="appContainer" data-reactid="6">
    <div data-reactroot="" data-reactid="1" data-react-checksum="1217500364">
      <header data-reactid="2">
        <h1 class="Header_title__1Zmri" data-reactid="3">A Web Application Journey</h1>
      </header>
      <nav data-reactid="4">
        <ul data-reactid="5">
          <li data-reactid="6"><a href="/" data-reactid="7">Home</a></li>
          <li data-reactid="8"><a href="/about" data-reactid="9">About</a></li>
        </ul>
      </nav>
      <main data-reactid="10">
        <p data-reactid="11">You are Home.</p>
      </main>
      <footer data-reactid="12">(c) 2016</footer>
    </div>
  </div><script src="/static/bundle.js" data-reactid="7"></script></body>
</html>