← Назад в блог

Create React App с несколькими точками входа

Опубликовано
4 мин чтения
--- просмотров

Иногда бывает так, что твой React-проект — это не просто одно классическое single-page приложение. Особенно если ты делаешь решение для уже существующего сайта, на котором раньше использовались разные фронтенд-технологии. Я был очень удивлён, что Create React App из коробки не позволяет решить эту проблему. С его помощью «из коробки» можно создать только single-page приложение и ничего больше. Кстати, один из react-разработчиков задавал тот же вопрос здесь:

Что ж, для меня это не самое предпочтительное решение. Потому что в будущем мы с командой планируем создавать новые react-приложения, где будут использоваться те же компоненты. И я хочу сохранить в проекте такую файловую иерархию:

Позволь объяснить логику:

  • Components — «глупые» Components/PureComponents/FunctionalComponents
  • Modules — функциональные части с взаимодействием с бэкендом, взаимодействием с redux-store, actions, selectors, sagas и так далее
  • Pages — (или views/scenes) — компонент, который агрегирует все компоненты для целевой страницы. Например, если у меня есть страница поиска, в SearchPage я подключу ещё 2 компонента: SearchInput, SearchResults и так далее. Это похоже на роуты, но разделённые по разным страницам (почти как server-rendering).

Что касается сборки, я хочу получить отдельный .html-файл и .js.bundle-файл для каждой точки входа в папке pages/. Тогда на продакшен-стенде я смогу вручную взять этот набор и подключить его на странице.

На скриншоте есть ещё много всего — карты, код, поделённый на чанки, и так далее, но, надеюсь, ты уловил идею :) Так что позволь показать, как я решил эту задачу: ### yarn eject

Эта команда извлекает («ejects») все конфигурационные скрипты из create-react-app. Используй её осторожно, потому что откатить это позже нельзя (только через git, конечно). После этого у тебя появятся новые папки с js-скриптами внутри:

config/paths.js

Здесь я добавил новую строку с массивом путей для всех моих страниц:

// 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

Здесь я добавил часть кода, которая создаёт новый экземпляр HtmlWebpackPlugin для каждой точки входа:

// 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
      )
    )
  );
});

А ещё тебе нужно включить этот массив в секцию plugins ниже (я использовал spread-оператор):

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

Также нужно поправить создание экземпляра ManifestPlugin. Тоже в секции 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,
          };
        },
      }),

И последнее, но не менее важное: добавь новые записи в секцию (не забудь заменить entry: entries, как показано на скриншоте):

let entries = {};

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

scripts/build.js

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

// techcode

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

Новая функция copyPublicFolder:

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

scripts/start.js

Новая функция для проверки файлов:
paths.appPages.forEach((appPage) => {
  if (!checkRequiredFiles([appPage.appHtml, appPage.appIndexJs])) {
    process.exit(1);
  }
});

Это всё! Ты крут!

Открыт для работы по контракту

Я доступен для работы по контракту. Если у вас есть интересная идея проекта — запишитесь на звонок через Calendly.

Записаться на 30-минутный звонок