Этот пост является частью серии сообщений для начинающих, предназначенных для людей, которые используют
готовые инструменты, шаблоны или шаблоны для React, но хотят с самого начала научиться и понимать, как создавать приложение React. .

Все публикации из этой серии:
Часть 1: Введение
Часть 2: Инициализация и первый файл
Часть 3: Использование синтаксиса ES2015 < br /> Часть 4: Применение руководства по стилю
Часть 5: Настройка сервера Express
Часть 6: Использование модуля Bundler
Часть 7 : Настройка React и лучшие практики
Часть 8: Настройка Redux
Часть 9: Настройка React Router
Часть 10: TDD и настройка Jest

Ваше приложение React может быть чисто интерфейсным, но что, если это не так? Что делать, если вам нужна серверная часть? Может быть, сделать какие-нибудь конфиденциальные вызовы API? Как насчет обработки и проверки отправленных форм?

Express.js или просто Express - это минимальная и гибкая платформа веб-приложений Node.js, которая предоставляет надежный набор функций для веб-приложений и мобильных приложений.

Вы можете легко создать каркас приложения с помощью инструмента Express Generator, но поскольку мы здесь, чтобы учиться, мы продолжим создание с нуля.

Примечание. Мы собираемся создать приложение почти так же, как его создает Express Generator, поэтому вы можете пропустить этот раздел и использовать генератор, если хотите.

Простая структура

Мы начнем с очень простой настройки и постепенно будем ее развивать, чтобы вы могли точно понять, что Express Generator добавляет в ваше приложение. Установите Express из npm как зависимость:

$ npm install --save express

Внутри файла index.js удалите образец кода, который у нас есть прямо сейчас, и импортируйте express:

import express from 'express';
// Express app setup
const app = express();

Для создания сервера вам понадобится модуль http из ядра Node.js:

import http from 'http';
import express from 'express';
// Express app setup
const app = express();
const server = http.createServer(app);
server.listen(3000);
server.on('listening', () => {
  console.log('Server is listening on port: 3000');
});

Метод http.createServer() принимает прослушиватель запросов (в данном случае это app, который по сути является просто функцией) и возвращает экземпляр сервера. Метод server.listen() запускает сервер на указанном порту, а затем мы просто записываем сообщение в консоль, как только сервер начинает прослушивание.

Если вы сейчас выполните npm start, чтобы запустить приложение, сервер запустится на порту 3000, но не сможет обрабатывать какие-либо запросы. Если вы попытаетесь перейти к http://localhost:3000/, вы получите общую404 ошибку.

Давайте создадим обработчик маршрута для обработки любого запроса и напечатаем сообщение Hello Express. Добавьте следующий код в файл index.js:

app.get('*', (req, res) => {
  res.end('Hello Express');
});

Метод app.get() - это метод маршрута для обработки запросов GET, отправленных серверу. Мы заявляем, что примем любой запрос GET (именно это означает звездочка) и ответим строкой «Hello Express».

Перезапустите приложение и попробуйте снова перейти к http://localhost:3000/. На этот раз вы увидите напечатанное «Hello Express». Если вы попытаетесь перейти по любому другому URL-адресу (например, http://localhost:3000/woohoo/) в том же домене, вы увидите то же сообщение.

Слишком просто масштабировать

Хотя такая настройка одного файла index.js работает, она далеко не идеальна и не масштабируется и не обслуживается. У вас не будет единственного обработчика маршрутов, у вас может быть так много маршрутов, что вам понадобится объект-маршрутизатор для обработки конечных точек вашего приложения.

Вы также не будете отображать представления своего приложения в виде строк, вместо этого вы должны использовать механизм шаблонов, и у вас также должен быть файл сервера, который обрабатывает запуск приложения и обрабатывает различные ошибки сервера.

Лучшая структура

Инструмент Express Generator создает более понятную архитектуру для вашего приложения. Чтобы следовать той же архитектуре, мы собираемся создать 4 новых каталога:

  • /server, который будет содержать файл сервера, запускающий приложение.
  • /routes, который будет содержать все обработчики маршрутов для приложения.
  • /views который будет содержать шаблоны, мы будем использовать Мопс в качестве движка.
  • /public, который будет содержать все общедоступные статические файлы, такие как изображения, файлы шрифтов, таблицы стилей, минифицированные скрипты и любые другие файлы, к которым можно получить публичный доступ.

Серверный файл

Начнем с серверного файла. Создайте новый файл в каталоге server и назовите его index.js. Этот файл будет новой точкой входа в приложение. Он будет нести ответственность за запуск сервера и обработку любых ошибок, которые могут возникнуть в процессе.

Это будет код, который входит в файл server / index.js:

/**
 * Module dependencies.
 */
import http from 'http';
import express from 'express';
/**
 * Express app setup
 */
const app = express();
/**
 * Simple logger function.
 */
function log(message) {
  process.stdout.write(`${message}\n`);
}
/**
 * Normalize a port into a number, string, or false.
 */
function normalizePort(val) {
  const port = parseInt(val, 10);
  if (Number.isNaN(port)) {
    // named pipe
    return val;
  }
  if (port >= 0) {
    // port number
    return port;
  }
  return false;
}
/**
 * Get port from environment and store in Express.
 */
const port = normalizePort(process.env.PORT || 3000);
app.set('port', port);
/**
 * Create HTTP server.
 */
const server = http.createServer(app);
let availablePort = port;
/**
 * Listen on provided port, on all network interfaces.
 */
function startServer(serverPort) {
  server.listen(serverPort);
}
/**
 * Event listener for HTTP server "error" event.
 */
function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }
  const bind = `${
    typeof port === 'string' ? 'Pipe' : 'Port'
  } ${port}`;
  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      log(`${bind} requires elevated privileges`);
      process.exit(1);
      break;
    case 'EADDRINUSE':
      if (availablePort - port < 10) {
        availablePort += 1;
        startServer(availablePort);
      } else {
        log(`${bind} is already in use`);
        process.exit(1);
      }
      break;
    default:
      throw error;
  }
}
/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
  const addr = server.address();
  const bind = `${
    typeof addr === 'string' ? 'pipe' : 'port'
  } ${
    typeof addr === 'string' ? addr : addr.port
  }`;
  log(`Server is listening on ${bind}`);
  log(`Visit: http://localhost:${addr.port}`);
}
/**
 * Start server.
 */
server.on('error', onError);
server.on('listening', onListening);
startServer(availablePort);

Я знаю, что это может показаться большим количеством кода, но не переусердствуйте. Встроенные комментарии объясняют, что делает каждая часть кода, но давайте разберемся, чтобы было понятнее:

  • Функция log(message) просто записывает сообщение в терминал, используя стандартный поток вывода.
  • Функция normalizePort(val) просто нормализует порт до числа, строки или значения false.
  • Функция startServer(serverPort) запускает сервер, используя указанный номер порта.
  • Функция прослушивателя onError обрабатывает ошибки сервера, но наиболее интересно то, что она обрабатывает ошибку занятого порта, проверяя, используется ли уже порт, и пытается перезапустить приложение на другом порту.
    Так, например, если вы попытаетесь запустить приложение на порту 3000, но этот порт уже занят, оно попытается перезапустить приложение на порту 3001. Если и это не удастся, оно попытается снова с портом 3002 и так далее.
  • Наконец, функция прослушивателя onListening просто записывает сообщение в терминал как уведомление о том, что сервер запущен и приложение успешно запущено.

Теперь измените сценарий «start» в package.json так, чтобы он указывал на файл /server/index.js:

{
  ...
  "start": "babel-node ./server/index.js",
  ...
}

После обновления файла package.json перезапустите приложение, чтобы увидеть изменения.

Примечание

Есть небольшая разница между тем, что мы сделали, и тем, что делает инструмент Express Generator. Когда вы создаете приложение Express с помощью команды express CLI, файл сервера создается как двоичный файл в / bin / www, а первая строка файла - это shebang:

#!/usr/bin/env node

Эта строка просто указывает системе использовать Node для запуска файла сервера. Это полезно, если ваше приложение предназначено для глобальной установки и использования. Вы должны указать двоичный файл в package.json следующим образом:

...
"bin": {
  "myapp": "./bin/www"
}
...

Затем каждый раз, когда ваше приложение устанавливается глобально:

$ npm install -g myapp

Его можно использовать как обычную команду:

$ myapp

Это также позволяет npx легко находить ваши двоичные файлы, когда ваше приложение или модуль установлены локально.

Создание файла app.js

В настоящее время единственным файлом, имеющим ссылку на экземпляр приложения Express, является файл сервера. Нам нужно будет добавить различные промежуточные продукты для обработки тела, маршрутизации, ведения журнала и других вещей, поэтому нам нужно иметь возможность import этот экземпляр приложения.

Мы можем сделать это, переместив экземпляр app в другой файл в корневом каталоге проекта:

/**
 * app.js
 */
import express from 'express';
// Express app setup
const app = express();
export default app;

Затем импортируйте этот экземпляр обратно в файл сервера:

/**
 * Module dependencies.
 */
import http from 'http';
import app from '../app'; // <-- import app
/**
 * Simple logger function.
 */
function log(message) {
  process.stdout.write(`${message}\n`);
}
...

Важное примечание:

Чтобы изменения в вашем коде имели место, вам придется перезапускать приложение каждый раз, когда вы редактируете и сохраняете файл JavaScript, связанный с сервером. Чтобы избежать перезапуска приложения вручную, вы можете использовать Nodemon.

Nodemon наблюдает за файлами в каталоге и, если какие-либо файлы изменяются, автоматически перезапускает приложение. Установите его как зависимость разработки:

$ npm install --save-dev nodemon

Затем добавьте сценарий dev в файл package.json:

{
  ...
  "dev": "nodemon --exec babel-node ./server/index.js"
  ...
}

Теперь каждый раз, когда вы хотите запустить приложение для разработки, используйте npm run dev вместо npm start, и когда вы вносите изменения в код сервера, Nodemon перезапускает приложение за вас.

Создание маршрутов и обработка запросов

Если вы запустите сервер сейчас и попытаетесь получить доступ к приложению, вы должны получить ту же общую 404 ошибку, что и раньше. Опять же, это просто потому, что у нас нет обработчиков маршрутов, которые отвечают на запросы, отправленные серверу.

Чтобы решить эту проблему, мы будем использовать Express Router. Создайте файл index.js внутри каталога routes:

import express from 'express';
const router = express.Router();
/* GET home page. */
router.get('*', (req, res) => {
  res.render('index');
});
export default router;

В этом файле мы создаем экземпляр маршрутизатора, который обрабатывает любые запросы (это означает звездочка), отправляемые приложению, отвечая одним представлением. Этот файл представления все еще не существует, но мы собираемся создать его через секунду.

Вернитесь к файлу app.js и измените код, чтобы использовать только что созданные маршруты:

import express from 'express';
import routes from './routes';
// Express app setup
const app = express();
// use routes
app.use('/', routes);
export default app;

Здесь следует отметить использование app.use() для подключения наших маршрутов к корневому пути приложения.

Настройка движка просмотра

Затем мы укажем расположение наших представлений и механизм представления, который должен использовать Express:

import path from 'path';
import express from 'express';
import routes from './routes/index';
// Express app setup
const app = express();
// view engine
app.set('views', path.join(__dirname, './views'));
app.set('view engine', 'pug');
// use routes
app.use('/', routes);
export default app;

Мы собираемся использовать Pug (ранее известный как Jade) в качестве движка просмотра, поэтому давайте установим его из npm в качестве зависимости:

$ npm install --save pug

Теперь создайте файл представления index.pug внутри каталога views:

doctype html
html
  head
    title Hello Express
  body
    h1 Hello Express

Снова перейдите к http://localhost:3000/, и вы увидите большое сообщение с надписью «Hello Express». Это очень простая страница, обычная страница, вероятно, будет содержать больше тегов в заголовке и теле документа для ключевых слов, таблиц стилей, скриптов и т. Д.

doctype html
html
  head
    meta(charset="UTF-8")
    meta(http-equiv="X-UA-Compatible", content="IE=edge")
    meta(name="viewport", content="width=device-width, initial-scale=1, minimum-scale=1")
    meta(name="description", content="")
    meta(name="keywords", content="")
    title= title
    link(rel="shortcut icon", href="/img/favicon.ico")
  body
    h1 Hello Express

Наследование шаблона

Если у вас есть другая страница, которая будет отображаться для другого маршрута, вам придется скопировать / вставить те же теги на новую страницу. К счастью, Pug предлагает способ правильно обрабатывать такие случаи, он называется Наследование шаблонов.

Создайте файл layout.pug:

doctype html
html
  head
    meta(charset="UTF-8")
    meta(http-equiv="X-UA-Compatible", content="IE=edge")
    meta(name="viewport", content="width=device-width, initial-scale=1, minimum-scale=1")
    meta(name="description", content="")
    meta(name="keywords", content="")
    title= title
    link(rel="shortcut icon", href="/img/favicon.ico")
  body
    block content

Затем упростите index.pug до следующего:

extends layout
block content
  h1 Hello Express

Вуаля! Теперь вы можете создать столько страниц, сколько вам нужно, просто расширяя этот макет.

Обработка ошибок сервера

При наших текущих настройках маршрутизатор будет обрабатывать все запросы, отвечая с помощью представления index.pug. Но что произойдет, если при ответе возникнет ошибка сервера? Измените обработчик маршрута, чтобы он выдавал преднамеренную ошибку:

router.get('*', (req, res) => {
  throw new Error('Oops');
  res.render('index');
});

Как видите, у нас действительно ужасная страница с ошибкой:

Давайте что-нибудь с этим сделаем, изменим файл app.js следующим кодом:

...
// error handlers
app.use((err, req, res, next) => {
  res.status(err.status || 500);
  res.render('error');
  next();
});
export default app;

Как вы, возможно, уже заметили, мы снова использовали app.use(), но на этот раз мы использовали его без указания маршрута, а сигнатура функции содержит четыре параметра. Это важно: вы должны предоставить четыре аргумента для Express, чтобы идентифицировать функцию как функцию промежуточного программного обеспечения для обработки ошибок.

Даже если вам не нужно использовать объект next, вы должны указать его для поддержки подписи с четырьмя параметрами. В противном случае функция будет интерпретироваться как обычное промежуточное ПО и не сможет обрабатывать ошибки.

Теперь давайте создадим нашу страницу с ошибкой /views/error.pug:

extends layout
block content
  h1 This is an error
  p Something went terribly wrong

Вот как будет выглядеть страница с ошибкой:

Я знаю, что эта страница с ошибкой не является картиной Пикассо или чем-то еще, но, по крайней мере, трассировка стека больше не отображается. Теперь вам может быть интересно, хорошо или плохо скрывать трассировку стека ошибок… и ответить на ваш вопрос: «Это зависит от обстоятельств».

Вы хотите видеть трассировку стека во время разработки и написания кода, но вы определенно не хотите, чтобы это отображалось вашим пользователям в производственной среде.

Давайте изменим наш код, чтобы скрыть трассировку стека в производственной среде и сохранить ее в среде разработки:

// error handlers
app.use((err, req, res, next) => {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: app.get('env') === 'development' ? err : {},
  });
  next();
});
export default app;

Шаблон страницы с ошибкой становится:

extends layout
block content
  h1= message
  h2= error.status
  pre #{error.stack}

Примечание. Не забудьте удалить строку, которая вызывает преднамеренную Oops ошибку в файле маршрутов: routes / index.js

Обработка ошибок 404

Чтобы обработать ошибки 404 и перейти к нашей настраиваемой странице ошибок, мы собираемся создать функцию промежуточного программного обеспечения, которая может обрабатывать любой запрос.

// use routes
app.use('/', routes); // <--- error 404 handler comes after this
// catch 404 and forward to error handler
app.use((req, res, next) => {
  const err = new Error('Not Found');
  err.status = 404;
  next(err);
});
// error handlers
app.use((err, req, res, next) => {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: app.get('env') === 'development' ? err : {},
  });
  next();
});
export default app;

Размещение функции промежуточного программного обеспечения после всех других обработчиков маршрутов важно. Если запрошенный маршрут может быть обработан приложением, один из обработчиков маршрута обработает его, в противном случае обработчик ошибки 404 получит запрос и отобразит нашу страницу ошибки.

Создание общедоступного каталога

Приложение будет обрабатывать только маршруты, которые вы определили в своем файле маршрутов, но мы собираемся использовать в нашем приложении множество статических ресурсов, таких как изображения, шрифты, видео, таблицы стилей, скрипты и т. Д.

Было бы бессмысленно создавать новый маршрут для каждого актива, который мы хотим использовать, поэтому мы собираемся создать единый каталог, в котором мы можем обслуживать эти статические ресурсы. Мы собираемся назвать этот каталог: public.

Express предоставляет очень удобную встроенную функцию промежуточного программного обеспечения, которая позволяет вам указать корневой каталог, из которого будут обслуживаться статические ресурсы.

Измените файл app.js:

// serve static files from 'public'
app.use(express.static(path.join(__dirname, './public')));
// use routes
app.use('/', routes);

Чтение переменных среды

Хранение конфигурации в среде отдельно от кода основано на методологии Двенадцатифакторного приложения. Для этого воспользуемся пакетом Dotenv.

Dotenv - это модуль с нулевой зависимостью, который загружает переменные среды из файла .env в process.env.

Установите Dotenv как зависимость:

$ npm install dotenv --save

Затем используйте Dotenv как можно раньше в своем коде:

import dotenv from 'dotenv';
// use dotenv
dotenv.config({
  silent: true,
});
...

Затем создайте файлы .env и .env.example в корневом каталоге приложения.

Примечание. Вы должны зафиксировать и отправить файл .env.example, но не файл .env.

Еще пара вещей

Прямо сейчас приложение получает запросы и отвечает на них без каких-либо следов запросов или каких-либо ошибок, которые могли возникнуть при ответе. Вам необходимо вести историю этих запросов и ошибок, регистрируя их, чтобы вы могли проверить их позже, когда вы столкнетесь с проблемой в производственной среде - да, это «когда», а не «если».

Morgan - популярная промежуточная функция регистратора HTTP-запросов для Node. Это регистратор по умолчанию, используемый экспресс-генератором.

Установите пакет как зависимость:

$ npm install --save morgan

Затем используйте функцию промежуточного программного обеспечения в файле app.js:

import logger from 'morgan';
// logger
app.use(logger('combined'));

Два других полезных пакета, используемых экспресс-генератором, - это Body Parser для анализа тел входящих запросов и Cookie Parser для синтаксического анализа заголовка cookie и заполнения запроса объектом, привязанным к именам файлов cookie.

Установите их оба как зависимости:

$ npm install --save body-parser cookie-parser

Тогда используйте их:

// body parser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// cookie parser
app.use(cookieParser());

Наконец-то…

Мы внесли много изменений в app.js, и вы можете запутаться или потеряться, поэтому вот как должен выглядеть полный файл:

/**
 * app.js
 */
import path from 'path';
import express from 'express';
import dotenv from 'dotenv';
import logger from 'morgan';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import routes from './routes';
// use dotenv
dotenv.config({
  silent: true,
});
// Express app setup
const app = express();
// view engine
app.set('views', path.join(__dirname, './views'));
app.set('view engine', 'pug');
// logger
app.use(logger('combined'));
// body parser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// cookie parser
app.use(cookieParser());
// serve static files from 'public'
app.use(express.static(path.join(__dirname, './public')));
// use routes
app.use('/', routes);
// catch 404 and forward to error handler
app.use((req, res, next) => {
  const err = new Error('Not Found');
  err.status = 404;
  next(err);
});
// error handlers
app.use((err, req, res, next) => {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: app.get('env') === 'development' ? err : {},
  });
  next();
});
export default app;

Заключение

Ваше приложение может быть простым, как один HTML-файл, но обычно это не так. Express - это популярный фреймворк для веб-приложений Node.js, и мы будем использовать его для создания наших серверов разработки и производства, а также для обработки маршрутизации на стороне сервера.

Была ли эта статья полезной? Пожалуйста, нажмите кнопку Хлопать ниже или подписывайтесь на меня, чтобы узнать больше.

Спасибо за прочтение! Если у вас есть отзывы, оставьте комментарий ниже.

Переходите к части 6: Использование сборщика модулей