Qué sucede dentro del complemento CSS Webpack:estilo de la web moderna

Dar estilo a una aplicación moderna no es una tarea sencilla:tradicionalmente se realiza sirviendo HTML con CSS para diseñar, mientras se agrega algo de JavaScript para realizar el trabajo.

¿Cómo modernizas este enfoque de configurar una aplicación? Podríamos pensar que sabemos la respuesta:usar un paquete como Webpack y un marco / biblioteca de JavaScript como React.

Pero, ¿cómo manejamos el CSS y por qué no es tan simple como cabría esperar?

Orden del día -

  • Parte 1:comprender el problema con CSS nativo.
  • Parte 2:configurar nuestra aplicación Webpack sin un complemento CSS.
  • Parte 3:escribir el cargador.
  • Parte 4:escribir un complemento avanzado.

Si está aquí solo para obtener información sobre la implementación, pase a la parte 3.

Descargo de responsabilidad:este no es un complemento listo para producción. Para ver uno que lo sea, consulte un proyecto en el que estamos trabajando mi equipo y yo:Stylable.

Parte 1:comprender el problema con CSS nativo.

Nuestras opciones

El CSS nativo se implementa de diferentes formas:

  • La primera (y la más sencilla) forma de incluir CSS es usar un estilo en línea, lo que significa que incluye explícitamente un estilo en una etiqueta HTML. <span style="color:red;">...</span>

  • Otra solución es usar una etiqueta HTML llamada <style>...</style> , donde su contenido de texto es el propio estilo y se utiliza para apuntar a los diferentes elementos HTML.

  • Y otra opción más es cargar un archivo CSS a través de una etiqueta de enlace y apuntar a los diferentes elementos HTML dentro de ese archivo.

Los problemas

Cada una de las soluciones anteriores tiene sus ventajas y desventajas. Es muy importante entenderlos para evitar comportamientos inesperados en tu estilismo. Sin embargo, encontrará que ninguna de esas soluciones resuelve uno de los problemas más problemáticos:que CSS es global .

El problema global es bastante difícil de superar. Digamos que tienes un botón con una clase llamada btn y le das estilo. Un día, su compañero de trabajo trabaja en una página diferente que también tiene un botón, y también decidió llamarlo btn. El problema debería ser evidente:los estilos chocarían.

Otro problema importante es la especificidad , donde la especificidad es igual entre selectores y se aplica al elemento la última declaración encontrada en el CSS. En pocas palabras, su pedido importa.

Parte 2:configurar nuestra aplicación Webpack sin un complemento CSS.

Las soluciones

Actualmente, existen muchas soluciones diferentes para estos problemas, desde frameworks de utilidades, preprocesadores de CSS y otras cosas que intentan ayudar con los problemas que tiene el CSS nativo.

En este artículo, me gustaría resolver algunos de esos problemas desde cero contigo.

Primero, configuremos nuestro entorno rápidamente. Para hacer esto, ejecute estos comandos:

(Creamos un directorio, inicializamos nuestro paquete.json e instalamos las dependencias de Webpack y Babel)

mkdir example-css-plugin
cd example-css-plugin
npm init -y
npm i -D webpack webpack-cli @webpack-cli/generators @babel/preset-react
npm i react react-dom

Cuando las dependencias de desarrollo hayan terminado de instalarse, ejecute el comando init de Webpack:

npx webpack init

Para nuestra configuración, sus respuestas deberían verse así:

? Which of the following JS solutions do you want to use? ES6
? Do you want to use webpack-dev-server? Yes
? Do you want to simplify the creation of HTML files for your bundle? Yes
? Do you want to add PWA support? No
? Which of the following CSS solutions do you want to use? none
? Do you like to install prettier to format generated configuration? No

Configurar Reaccionar

Ir a .babelrc y asegúrese de que la matriz de ajustes preestablecidos incluya "@babel/preset-react".

Esto no es obligatorio, pero es para asegurarse de que nuestro proyecto pueda transformar jsx.

{
    "plugins": ["@babel/syntax-dynamic-import"],
    "presets": [
        [
            "@babel/preset-env",
            {
                "modules": false
            }
        ],
            "@babel/preset-react"
    ]
}

Ahora debemos ir a index.html y asegurarnos de que tenga el div con la identificación de "root".

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>CSS Webpack Plugin example</title>
    </head>
    <body>
        <div id="root"></div>
    </body>    
</html>

Después de todo eso, estamos listos para escribir nuestra aplicación dentro de src/index.js :

import React from 'react';
import { render } from "react-dom";

render(
  <div>
    Hello World!
  </div>,
  document.getElementById('root')
)

Parte 3:escribir el cargador

Entonces, ¿a qué apuntamos? Lo primero es lo primero, queremos simplemente cargar nuestro CSS desde nuestro JS.
Vamos a crear nuestro archivo CSS y llamarlo index.css .

.app {
    background: red;
}

Y por supuesto, utilízalo en el index.js expediente:

import React from 'react';
import { render } from 'react-dom';
import './index.css'
​
render(
  <div className="app"> Hello World! </div>,
  document.getElementById('root')
);

Ejecuta nuestra aplicación:

npm run serve

Ahora probablemente vea este error en la consola:


Este error tiene mucho sentido, ya que Webpack no sabe cómo manejar las importaciones de CSS; debemos decirle cómo hacerlo.

Creación de un cargador de paquetes web

¿Qué son los cargadores?

Webpack permite el uso de cargadores para preprocesar archivos. Esto le permite agrupar cualquier recurso estático mucho más allá de JavaScript.
En pocas palabras, en nuestro caso, son funciones que toman el archivo CSS como entrada y generan un archivo js.
CSS -> JS

Implementación del cargador

Vamos a crear un archivo junto con el webpack.config.js llamado loader.js .
Nuestro objetivo es agregar el valor de estilo que obtenemos del archivo CSS dentro del dom.
loader.js :

// Appending the style inside the head
function appendStyle(value) {
    const style = document.createElement('style');
    style.textContent = value;
    document.head.appendChild(style);
}
​
// Make sure it is not an arrow function since we will need the `this` context of webpack
function loader(fileValue) {
  // We stringify the appendStyle method and creating a file that will be invoked with the css file value in the runtime
  return `
    (${appendStyle.toString()})(${JSON.stringify(fileValue)})
  `
}
​
module.exports = loader;
​

Ahora necesitamos registrarlo dentro de la configuración del paquete web.
webpack.config.js :

const config = {
  //... rest of the config
    module: {
        rules: [
          // ... other rules not related to CSS
            {
                test: /\.css$/,
                loader: require.resolve('./loader')
            }
        ]
    }
  // ...
}

Reinicie la terminal, ¡y lo tenemos! 🎊

¿Qué sucede detrás de escena?

Webpack ve tu importación de CSS dentro de index.js . Busca un cargador y le da el valor de JavaScript que queremos evaluar en tiempo de ejecución.

Superar el problema global

Ahora tenemos nuestro estilo, pero todo es global. Todos los demás idiomas resuelven el problema global con alcance o espacio de nombres. CSS, por supuesto, no es un lenguaje de programación per se, pero el argumento sigue siendo válido.
Implementaremos la solución de espacio de nombres. Esto nos dará un alcance, y cada archivo tendrá su propio espacio de nombres.
Por ejemplo, nuestra importación se verá así:

AppComponent123__myClass

Si otro componente tiene el mismo nombre de clase, no importará detrás de escena ya que el espacio de nombres será diferente.
Vamos al loader.js y agregue el siguiente método:

const crypto = require('crypto');
​
/**
 * The name is the class we are going to scope, and the file path is the value we are going to use for namespacing.
 * 
 * The third argument is the classes, a map that points the old name to the new one.
 */
function scope(name, filepath, classes) {
  name = name.slice(1); // Remove the dot from the name.
  const hash = crypto.createHash('sha1'); // Use sha1 algorithm.
  hash.write(filepath); // Hash the filepath.

  const namespace = hash.digest('hex').slice(0, 6); // Get the hashed filepath.
  const newName = `s${namespace}__${name}`;
​
  classes[name] = newName; // Save the old and the new classes.
​
  return `.${newName}`
}

Una vez que hayamos terminado de definir el alcance de la clase, devolvamos el método del cargador.
Necesitamos una forma de conectar el selector de clase con ámbito al código javascript del usuario.

function loader(fileValue) {
  const classes = {}; // Map that points the old name to the new one.
  const classRegex = /(\.([a-zA-Z_-]{1}[\w-_]+))/g; // Naive regex to match everything that starts with a dot.
  const scopedFileValue = fileValue.replace(classRegex, (name) => scope(name, this.resourcePath, classes)); // Replace the old class with the new one and add it to the classes object
​
 // Change the fileValue to scopedFileValue and export the classes.
  return `
    (${appendStyle.toString()})(${JSON.stringify(scopedFileValue)})
​
    export default ${JSON.stringify(classes)}
  ` // Export allows the user to use it in their javascript code
}

En el index.js , ahora podemos usarlo como un objeto:

import React from 'react';
import { render } from "react-dom";
import classes from './index.css'; // Import the classes object.
​
render(
  <div className={classes.app /* Use the app class  */}>
    Hello World
  </div>,
  document.getElementById('root')
)

Ahora funciona con el selector de espacio de nombres 🎉
Clase con selector de espacio de nombres
Algunos puntos importantes sobre los cambios que implementamos.

  • Cuando Webpack utiliza el cargador, el contexto será el contexto del cargador (this ) de Webpack. Puedes leer más sobre esto aquí. Proporciona la ruta del archivo resuelto, lo que hace que el espacio de nombres sea único para el archivo.

  • La forma en que extraemos los selectores de clases del archivo CSS es una implementación ingenua que no tiene en cuenta otros casos de uso. La forma ideal es usar un analizador CSS.

  • this.resourcePath se refiere a la ruta local, lo que significa que en otras máquinas, la ruta puede verse diferente.

    El cargador ahora está implementado y tenemos clases con ámbito en este punto. Sin embargo, todo se carga desde JavaScript, por lo que aún no es posible almacenar en caché el CSS.

    Para hacer esto, necesitaremos componer todo el CSS en un solo archivo, y para hacer eso, necesitaremos crear un complemento de Webpack.

    Parte 4:escribir un complemento avanzado


    Como se mencionó anteriormente, implementamos un cargador que puede inyectar CSS en nuestra página. ¿Qué pasa si queremos hacerlo con un solo archivo y no con una inyección?

    Cargar CSS como un archivo tiene muchos beneficios, y el mejor de ellos es el almacenamiento en caché. Un navegador puede almacenar en caché ese archivo y no necesitará volver a descargarlo cada vez que sea necesario.

    Esta operación es más complicada que el caso del cargador, ya que tendremos más contexto sobre el proceso de agrupación de Webpack.

¿Qué es un complemento?


Un complemento Webpack es un objeto de JavaScript que tiene un método de aplicación. El compilador de Webpack llama a este método de aplicación, lo que le da acceso a todo el ciclo de vida de la compilación.

Creación del complemento


Vamos a crear un archivo llamado plugin.js y crea el esqueleto del complemento:

​
class CSSPlugin {
  cssMap = new Map() // We will save the CSS content here
​
  /**
   * Hook into the compiler
   * @param {import('webpack').Compiler} compiler 
   */
  apply(compiler) { }
}
​
module.exports = {
  CSSPlugin
}
​

Ahora implementemos el método apply:

​
class CSSPlugin {
  cssMap = new Map() // We will save the CSS content here
​
  /**
   * Hook into the compiler
   * @param {import('webpack').Compiler} compiler 
   */
  apply(compiler) {
​
    // Hook into the global compilation.
    compiler.hooks.thisCompilation.tap('CSSPlugin', (compilation) => {
​
      // Hook into the loader to save the CSS content.
      compiler.webpack.NormalModule.getCompilationHooks(compilation).loader.tap(
        'CSSPlugin',
        (context, module) => {
​
          // Setting up a method on the loader context that we will use inside the loader.
          context.setOutputCSS = (css) => {
​
            // the key is the resource path, and the CSS is the actual content.
            this.cssMap.set(module.resource, css)
          }
        }
      )
    })
   }
}

Nos conectamos a la compilación global y luego nos conectamos al cargador (que se implementó anteriormente).

Cuando se puede acceder al contenido del cargador, agregamos el método setOutputCSS para llamarlo desde el cargador.

Aquí se explica cómo llamar a este método en loader.js :

function loader(fileValue) {
  const classes = {}; // Map that points the old name to the new one.
  const classRegex = /(\.([a-zA-Z_-]{1}[\w-_]+))/g; // Naive regex to match everything that starts with a dot.
  const scopedFileValue = fileValue.replace(classRegex, (name) => scope(name, this.resourcePath, classes)); // Replace the old class with the new one and add it to the classes object
​
  this.setOutputCSS(scopedFileValue) // Pass the scoped CSS output
​
 // Export the classes.
  return `export default ${JSON.stringify(classes)}`
}

Como puede ver, no agregamos el estilo en JavaScript. Usamos el método que agregamos al contexto.

Después de recopilar todo el contenido de CSS con ámbito, ahora debemos conectarnos al enlace del proceso de activos para que el compilador sepa que tenemos un nuevo activo que debe manejar.

Vamos a agregarlo al método de aplicación:

class CSSPlugin {
  // ...
​
  apply(compiler) {
      compiler.hooks.thisCompilation.tap(
        'CSSPlugin', 
        (compilation) => {
        // ...
​
        // Hook into the process assets hook
        compilation.hooks.processAssets.tap(
          {
            name: 'CSSPlugin',
            stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_DERIVED
          },
          () => {
​
                // Loop over the CSS content and add it to the content variable
                let content = '';
                for (const [path, css] of this.cssMap) {
                  content += `/* ${path} */\n${css}\n`;
                }
​
                // Append the asset to the entries.
                for (const [name, entry] of compilation.entrypoints) {
                  assetName = `${name}.css`;
                  entry.getEntrypointChunk().files.add(assetName);
                }
​
                // Create the source instance with the content.
                const asset = new compiler.webpack.sources.RawSource(content, false);
​
                // Add it to the compilation
                compilation.emitAsset(assetName, asset);
          }
      )
  }
}


Ahora ejecutaremos el comando de compilación:

npm run build

Deberíamos ver main.css en la carpeta de salida y también inyectado en el HTML:

Salida:

index.html :

¡Y eso es!
Terminamos el complemento y tenemos un archivo CSS para todo el CSS.

Tenga en cuenta que omitimos las dependencias, el orden de gráficos y el filtrado de CSS no utilizado con fines de demostración.

Puede ver mi implementación completa con mecanografiado y pruebas en este repositorio aquí.

Si tienes alguna pregunta, puedes contactarme a través de LinkedIn. Espero haber podido ayudarte.