Создание и настройка проекта React + Webpack с нуля до SSR

Создание и настройка проекта React + Webpack с нуля до SSR

Хочу вам показать наглядный пример и инструкцию того, как можно самому с «чистого листа» сконфигурировать Webpack для React и Server Side Render'а без каких-либо бойлерплейтов, вроде create-react-app.

12
\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Выполняем в консолях npm run watch и видим результат

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"6b30e5cd-6248-5cff-69b5-a57dc0f23c6a","width":1372,"height":800,"size":938946,"type":"png","color":"2b2b2b","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Берем тестовый React-контент с GitHub'а, где я сделал страницы с роутингом и стили. Кстати, заметьте, стили я подключаю через require в самом компоненте (в методе render для классовых компонентов). Это позволяет в head добавлять стили только отображаемых компонентов, что снижает нагрузку на парсинг и рендер.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Представим, что наше приложение полностью написано и готово к релизу. Приступим к Server Side Render части.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для работы SSR нам нужны следующие npm-пакеты:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["express — Node JS сервер","@types/express — TS тип для ExpressJS
","mini-css-extract-plugin — плагин, который выгрузит стили не в JS-файлы, а отдельный CSS. Если понадобится, вы можете вывести его в серверную часть.
","webpack-node-externals — плагин, позволяющий исключать из сборки node_modules по дефолту
"],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Выполняем

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"npm i express @types/express mini-css-extract-plugin webpack-node-externals --save-dev","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В webpack.config.js добавим:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"const MiniCssExtractPlugin = require('mini-css-extract-plugin')","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В массив модулей добавляем новый объект для компиляции отдельного изоморфного CSS-файла:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"stylusIsomorph: {\n test: /\\.styl$/,\n use: [\n {\n loader: MiniCssExtractPlugin.loader,\n },\n {\n loader: \"css-loader\",\n },\n {\n loader: \"stylus-loader\",\n options: {\n import: [\n path.resolve(__dirname, './src/Common/Styles/variables.styl'),\n ],\n }\n },\n ],\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И также добавим для него PostCSS для production-режима:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"if (env === 'production') {\n ...\n modules.stylusIsomorph.use.splice(2, 0, { loader: \"postcss-loader\" })\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Казалось, мы могли бы все это запустить напрямую в ExpressJS, но нам надо все приложение прогнать через TS.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"f402fc96-997f-3f8f-a725-c5c7ac31a2b2","width":583,"height":565,"size":1035042,"type":"png","color":"383c43","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создадим в корне проекта отдельный Webpack-конфиг для компиляции серверной части webpack.server.js:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"const path = require(\"path\")\nconst UglifyJsPlugin = require('uglifyjs-webpack-plugin')\nconst nodeExternals = require('webpack-node-externals')\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin')\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst webpackConfig = require('./webpack.config')\n\nmodule.exports = (env, argv) => {\n const modeEnv = argv.mode || 'development'\n const config = webpackConfig(modeEnv)\n\n const optimizations = {\n minimizer: [\n new UglifyJsPlugin(),\n ],\n }\n\n return {\n plugins: [\n new CleanWebpackPlugin(),\n new MiniCssExtractPlugin(), // Подключаем плагин для CSS\n ],\n resolve: config.resolve,\n module: {\n rules: [\n config.modules.js,\n config.modules.stylusIsomorph,\n ],\n },\n entry: {\n main: './src/Server.tsx', // Тут уже энтрипоинт сервера, который сделаем далее\n },\n output: {\n filename: '[name].js',\n path: path.resolve(__dirname, 'server'), // Все компилируем в папку server\n },\n performance: {\n hints: false,\n },\n optimization: optimizations,\n target: 'node', // обязательно указываем режим сборки для node js, а не браузера\n externals: [nodeExternals()], // исключаем node_modules\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создадим наш энтрипоинт для сборки сервера src/Server.tsx:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import fs from 'fs'\nimport express from 'express'\nimport React from 'react'\nimport ReactDOMServer from 'react-dom/server'\nimport { StaticRouter } from 'react-router'\nimport { App } from 'App'\nimport { Html } from './Html/Server'\n\nconst port = 3000\nconst server = express()\nconst jsFiles: Array = []\n\nfs.readdirSync('./dist/assets').forEach(file => {\n if (file.split('.').pop() === 'js') jsFiles.push('/assets/' + file)\n})\n\nserver.use('/assets', express.static('./dist/assets'))\n\nserver.get('*', async (req, res) => {\n ReactDOMServer.renderToNodeStream(\n \n \n \n ).pipe(res)\n})\n\nserver.listen(port, () => console.log(`Listening on port ${port}`))","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

1) «Обертку» HTML'а теперь генерируем динамически.

2) Обратите внимание на StaticRouter. ExpressJS будет автоматически генерировать роутер для отдачи статичного HTML с сервера.

3) В тело документа напрямую «опрокидываем» наш энтрипоинт приложения.

4) GET-метод реализуем через Node JS Streams.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создадим для начала HTML в src/Html/Server.tsx:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import React from 'react'\n\ninterface Html {\n scripts: Array\n}\n\nexport function Html({ children, scripts }: React.PropsWithChildren) {\n return (\n \n \n \n \n \n React Starter Pack\n \n \n
{children}
\n {scripts.map((script, index) =>