lunes, 30 de diciembre de 2019

Sitio estático multi-lenguaje con webpack

Recientemente, he tenido que configurar una plantilla de sitio web estático (digo plantilla porque supuestamente servirá como scaffolding a futuros proyectos, pero seguro quedará obsoleta en 2 días y habrá que modificarla, veremos a posteriori si sirvió a más de un proyecto). El sitio web no es una SPA, es una MPA (Multiple-Page Application) y, además, era un requisito indispensable la traducción a múltiples lenguajes y que fuera en tiempo de compilación, por SEO. Siendo así, y después de no encontrar nada en google ya hecho y que me gustara, pensé que sería fácil y rápido hacerlo con webpack, pero craso error, me ha llevado más tiempo de lo que suponía y he acabado (no sé si con razón o sin razón), buceando en exceso en la documentación, y probando con el método ensayo-error el resultado de la compilación. La conclusión que saco de todo esto es que en el front me siento como un conductor novel que está configurando un vehículo con un excesivo y apabullante número de extras disponibles. De hecho, y por casualidad, hace unas semanas contesté a esta encuesta https://stateofjs.com/ y viendo ahora los resultados más del 50% contestamos que sí, que "La creación de aplicaciones JavaScript es demasiado compleja en este momento" https://2019.stateofjs.com/opinions/#building_js_apps_overly_complex, aunque claro, más del 50% eramos full-stack https://2019.stateofjs.com/demographics/#jobTitle así que lo mismo el problema no es javascript, si no gente que le da a todo y así no se puede (yo me incluyo por si no ha quedado claro).

En este post, me quiero centrar en las decisiones que he tomado en relación con la MPA y a la traducción. Para el resto es más fácil y seguro leer la documentación de webpack.

El repositorio con la plantilla está en https://github.com/panicoenlaxbox/webpack-static-site-template

Si tenemos una MPA, tendremos varios entries y por cada uno de ellos podremos decidir que bundles queremos incluir, la idea está sacada de https://webpack.js.org/guides/entry-advanced/

entry: {
index: [
    "./src/index.js",
    "./src/styles/index.scss",
    "selectric/public/selectric.css"
],
about: ["./src/about.js", "./src/styles/about.scss"]
},
plugins: [
new HtmlWebpackPlugin({
    filename: path.join(translation.dist, "index.html"),
    template: "src/index.html",
    chunks: ["index", "vendor"]
}),
new HtmlWebpackPlugin({
    filename: path.join(translation.dist, "about.html"),
    template: "src/about.html",
    chunks: ["about", "vendor"]
}),

Fijarse que cada entry especifica los estilos, .scss por lo que no usa require como dependencia en el .js. Además, cada nueva página debería ir acompañada de una nueva entry y una nueva instancia de HtmlWebpackPluginhttps://github.com/jantimon/html-webpack-plugin#generating-multiple-html-files.

Por otro lado, ya ha aparecido el objeto translation. Aunque se usa i18n-webpack-plugin para traducir las claves de los ficheros .js, también hay claves de traducción en ficheros .html y ahí el reemplazo lo he resuelto con este otro plugin html-string-replace-webpack-plugin-webpack-4.

new HtmlStringReplace({
    patterns: [
    {
        match: /__(.+?)__/g,
        replacement: (match, $1) => translation.translation[$1]
    }
    ]
}),

Como que el sitio es estático, quería obtener en la raíz de dist/ la versión del lenguaje neutro y luego una carpeta / por cada lenguaje soportado. Para ello, he usado una compilación mútiple (la idea está sacada de https://survivejs.com/webpack/techniques/i18n/) y luego unas tareas extras para ajustarlo todo.

Que webpack devuelve una función en vez de un objeto, se puede ver en https://webpack.js.org/configuration/configuration-types/. Esto da mucho juego y abre distintas posibilidades.

module.exports = (env, argv) => {
  const isProduction = argv.mode === "production";
  return translations.map(translation => {
    return {
      entry: {

translations es un objeto que lee los mismos ficheros que usa … y agrega algunas propiedaes útil para la compilación.

[ { language: 'en',
    translation:
     { language: 'en',
       title: 'My static site template',
       message: 'My message' },
    default: true,
    dist: 'C:\\Temp\\webpack-static-site-template\\dist' },
  { language: 'es',
    translation:
     { language: 'es',
       title: 'Mi plantilla de sitio estático',
       message: 'Mi mensaje' },
    default: false,
    dist: 'C:\\Temp\\webpack-static-site-template\\dist\\es' } ]

Ahora cada vez que webpack emite un bundle lo hace teniendo en cuenta el lenguaje:

output: {
  path: translation.dist,
  filename: `[name].${translation.language}${
      isProduction ? ".[contenthash]" : ""
  }.js`
}

Como estamos compilando lo mismo varias veces (con la única diferencia del lenguaje), acabaremos por tener en dist/ algo válido pero un poco feo y repetitivo, se puede mejorar haciendo algunos reemplazos (que por otro lado no me gusta hacer, es como hackear el sistema, de largo es lo que más oscuro me parece).

new HtmlStringReplace({
    patterns: [
        {
            match: /(<img src=")(?!(\/\/|https?:\/\/|data:image))/gi,
            replacement: (match, $1) => `${$1}/`
        }
    ]
}),
new HtmlStringReplace({
    enable: !translation.default,
    patterns: [
        {
            match: /(<link href=")(?!(\/\/|https?:\/\/))/gi,
            replacement: (match, $1) => `${$1}../`
        },
        {
            match: /(<script type="text\/javascript" src=".*?)(?=vendor\.)/gi,
            replacement: (match, $1) => {
            return $1.substring(0, $1.lastIndexOf('"') + 1) + "/";
            }
        }
    ]
}),

Y borrando por último, lo que no queremos en un hook del ciclo de compilación:

new EventHooksPlugin({
    done: () => {
        if (!translation.default) {
            exec(`rimraf \"dist/${translation.language}/!(*.html|*.js)\"`);
            exec(`rimraf \"dist/${translation.language}/vendor*.js"`);
        }
    }
})

Después de esto, acabaremos con una estructura como la siguiente:

│   about.css
│   about.en.js
│   about.html
│   index.css
│   index.en.js
│   index.html
│   vendor.css
│   vendor.js
└───es
        about.es.js
        about.html
        index.es.js
        index.html

Un saludo!

No hay comentarios:

Publicar un comentario