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-минутный звонок