← Volver al blog

Create React App con múltiples puntos de entrada

Publicado el
4 min de lectura
--- vistas

A veces pasa que tu proyecto React no contiene solo una única aplicación single-page. Especialmente, si estás creando una solución para un sitio web ya desarrollado con diferentes tecnologías frontend usadas antes en él. Me sorprendió mucho que Create React App no te permite resolver este problema. Con su ayuda de fábrica, solo puedes crear una single-page application y nada más. Además, uno de los desarrolladores react hace la misma pregunta aquí:

Bueno, no es la solución preferida para mí. Porque, en el futuro, yo con mi equipo tenemos planes de crear alguna nueva aplicación react donde se usarán los mismos componentes. Y quiero mantener la jerarquía de archivos en el proyecto de esta manera:

Déjame explicar la lógica:

  • Components - Components/PureComponents/FunctionalComponents tontos
  • Modules - partes de funcionalidad con interacciones de backend, interacciones con el redux-store, actions, selectors, sagas y demás
  • Pages - (o views/scenes) - Component que agrupa todos los componentes para una página objetivo. Por ejemplo, si tengo una página de búsqueda, en SearchPage incluiré otros 2 componentes: SearchInput, SearchResults y demás. Es como rutas pero divididas en páginas diferentes (igual que server-rendering).

Como cosa de build, quiero tener un archivo .html separado con un archivo .js.bundle para cada entrada en la carpeta pages/. Así en un ambiente de producción puedo tomar este conjunto manualmente e incluirlo en la página.

En la pantalla hay muchas otras cosas como mapas, código dividido en chunks y demás, pero espero que hayas captado la idea :) Entonces, déjame mostrarte cómo resuelvo esta tarea: ### yarn eject

Este comando expulsa todos los scripts de configuración de create-react-app. Úsalo con cuidado porque no puedes volver atrás después (solo con git, claro). Después de eso tendrás algunas carpetas nuevas con scripts js dentro:

config/paths.js

Aquí agregué una nueva línea con un array de rutas para todas mis páginas:

// config after eject: we're in ./config/
module.exports = {
  dotenv: resolveApp('.env'),
  appPath: resolveApp('.'),
  appBuild: resolveApp('build'),
  appPublic: resolveApp('public'),
  // techcode
  appPages: [
    {
      name: modalUpdateOperationalPlan,
      title: ModalUpdateOperationalPlan,
      appHtml: resolveApp(public / modalUpdateOperationalPlan.html),
      appIndexJs: resolveApp(src / pages / ModalUpdateOperationalPlan / index.tsx),
    },
  ],
  appPackageJson: resolveApp('package.json'),
  appSrc: resolveApp('src'),
  appTsConfig: resolveApp('tsconfig.json'),
  appJsConfig: resolveApp('jsconfig.json'),
  yarnLockFile: resolveApp('yarn.lock'),
  testsSetup: resolveModule(resolveApp, 'src/setupTests'),
  proxySetup: resolveApp('src/setupProxy.js'),
  appNodeModules: resolveApp('node_modules'),
  publicUrl: getPublicUrl(resolveApp('package.json')),
  servedPath: getServedPath(resolveApp('package.json')),
};

config/webpack.config.js

Aquí agregué parte del código que crea una nueva instancia de HtmlWebpackPlugin para cada entrada:

// techcode
const htmlPlugins = [];

paths.appPages.forEach((appPage) => {
  htmlPlugins.push(
    new HtmlWebpackPlugin(
      Object.assign(
        {},
        {
          inject: true,
          template: appPage.appHtml,
          filename: `${appPage.name}.html`,
          title: appPage.title,
          chunks: [appPage.name],
        },
        isEnvProduction
          ? {
              minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
              },
            }
          : undefined
      )
    )
  );
});

Y, además, necesitas incluir este array en la sección plugins de abajo (usé el operador spread):

<pre class="lang:js" decode:true="">plugins: [
      ...htmlPlugins,

También necesitas arreglar la creación de la instancia de ManifestPlugin. También en la sección plugins:

[]

/ Generate an asset manifest file with the following content:
      // - files key: Mapping of all asset filenames to their corresponding
      //   output file so that tools can pick it up without having to parse
      //   `index.html`
      // - entrypoints key: Array of files which are included in `index.html`,
      //   can be used to reconstruct the HTML if necessary
      new ManifestPlugin({
        fileName: 'asset-manifest.json',
        publicPath: publicPath,
        generate: (seed, files, entrypoints) => {
          const manifestFiles = files.reduce((manifest, file) => {
            manifest[file.name] = file.path;
            return manifest;
          }, seed);

          // techcode
          let entrypointFiles = [];

          for (let [entryFile, fileName] of Object.entries(entrypoints)) {
            let notMapFiles = fileName.filter(fileName => !fileName.endsWith('.map'));
            entrypointFiles = entrypointFiles.concat(notMapFiles);
          };

          return {
            files: manifestFiles,
            entrypoints: entrypointFiles,
          };
        },
      }),

Y por último pero no menos importante: agrega nuevas entradas a la sección (no olvides reemplazar entry: entries como se muestra en la pantalla):

let entries = {};

paths.appPages.forEach((appPage) => {
  entries[appPage.name] = [
    appPage.appIndexJs,
    isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'),
  ].filter(Boolean);
});

scripts/build.js

Cambié una parte del código donde se verifica la existencia de archivos:

// techcode

paths.appPages.forEach((appPage) => {
  if (!checkRequiredFiles([appPage.appHtml, appPage.appIndexJs])) {
    process.exit(1);
  }
});

Nueva función copyPublicFolder:

function copyPublicFolder() {
  paths.appPages.forEach((appPage) => {
    fs.copySync(paths.appPublic, paths.appBuild, {
      dereference: true,
      filter: (file) => file !== paths.appHtml,
    });
  });
}

scripts/start.js

Nueva función para verificación de archivos:
paths.appPages.forEach((appPage) => {
  if (!checkRequiredFiles([appPage.appHtml, appPage.appIndexJs])) {
    process.exit(1);
  }
});

¡Eso es todo! ¡Eres increíble!

Disponible para colaboración por contrato

Estoy disponible para colaborar por contrato. Si tiene una idea de proyecto interesante, reserve una llamada por Calendly.

Agenda una llamada de 30 min