Обновление: 18 июня 2019 г .:

Оказывается, да, это научит вас, как раскручивать Node API. Однако с момента публикации я научился лучше разделять свои опасения или гарантировать, что каждый компонент несет единую ответственность. (S в SOLID) Я надеюсь опубликовать исправительную историю в течение одной недели.

Я с нетерпением жду возможности поучиться вместе с вами.

[Неотредактированный исходный текст после сгиба.]

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

Что вы должны знать заранее:

  • Что такое JSON
  • Как вводить команды в оболочку (командная строка)
  • Базовый синтаксис и терминология JavaScript (ES6 плюс, если нет, вы подберете его)

Что вы должны сделать в первую очередь:

Чего мы не будем делать в этом руководстве: писать любой HTML-код (мы расскажем об этом в одной из следующих статей). Мы будем разрабатывать серверный API. Он будет обрабатывать HTTP-запросы от браузера (или Postman, или наших тестов) и отвечать данными из нашей базы данных и всем остальным, что мы хотим сказать. Это веб-сервер или веб-сервис. Вы можете предоставить кому-нибудь документацию по вашему API, и он сможет создать свой собственный веб-сайт или мобильное приложение, используя ваш сервис.

Я использую Node.js версии 8.11.1. Я уверен, что это будет работать в будущих версиях. Но не могло быть в предыдущих версиях.

Если хотите увидеть готовый код, переходите сюда в репо.

Я всегда начинаю с нового репозитория GitHub. Зайдите в свой и создайте его. Называйте это как хотите. НЕ инициализируйте репозиторий с помощью README. В этом руководстве мы назовем его «портапи», так как это API для моего портфолио. Я изо всех сил пытался решить, включать ли Git в это руководство или нет, и решил сделать это, чтобы сделать его немного ближе к промежуточному руководству, а не к изобилию кода для начинающих.

Откройте командную строку и перейдите туда, где вы хотите разрабатывать приложения. Нам нужно создать новый каталог.

$ mkdir portapi

Не вводите знаки доллара во фрагментах кода. Они представляют собой командную строку Git bash.

Теперь перейдите в ваш новый каталог.

$ cd portapi

Затем введите эти команды, чтобы создать файл README. Инициализируйте новый локальный каталог как пустой репозиторий Git. Подготовьте README для меня. Запишите изменения с сообщением о фиксации. Добавить новый пульт

$ echo "# Hello, portapi!" >> README.md
$ git init
$ git add README.md
$ git commit -m "first commit"
$ git remote add origin https://github.com/<YOUR_GITHUB_NAME>/portapi.git
$ git push -u origin master

Вернитесь к своему репо на Github. В нем должен быть README и написано Hello, portapi!. Я привык делать это каждый раз, когда начинаю какой-нибудь проект. Примечание: вам нужно будет ввести свои учетные данные GitHub, чтобы продолжить. Я сохранил свой в Git Bash.

Вернитесь в командную строку типа.

$ npm init

Это начинает процесс создания файла package.json для нашего проекта. Просто нажимайте ввод для каждого запроса, кроме точки входа. Введите «server.js». Почему? Это не имеет большого значения, но мне нравится думать, что это веб-сервер, а не полноценное приложение. Хотя иногда я называю это приложением.

Перейдите в свой текстовый редактор (мне нравится Atom) и откройте только что созданный package.json. Мы собираемся добавить некоторые зависимости. Некоторые люди предпочитают устанавливать зависимости в командной строке. Иногда я тоже. Но для такого проекта я предпочитаю добавить их в package.json и установить все сразу. После последнего поля (мое говорит, что домашняя страница, ваше будет другим, если вы не использовали git init), добавьте следующие поля.

"dependencies": {},
"devDependencies": {}

Зависимости - это другие пакеты Node, которые необходимы для работы вашего приложения или пакета. DevDependencies - это пакеты, которые помогают вам разрабатывать приложение и не являются необходимыми для функциональности вашего приложения. Затем мы заполним эти два объекта всем, что нам понадобится, начиная с DevDependencies.

// package.json
"devDependencies": {
    "chai": "4.1.2",
    "chai-http": "4.0.0",
    "mocha": "5.1.1",
    "nodemon": "1.17.3"
}

Что это за пакеты, которые нам нужны для разработки нашего приложения? Первые три предназначены для тестирования. Я пришел к выводу, что автоматизация тестов с первого дня / с первого дня делает все проще в дальнейшем. И вам не нужно загружать Postman (хотя вам нужно).

Nodemon - это модуль, который автоматически перезапускает ваш сервер каждый раз, когда вы нажимаете CTRL + S (что должно происходить часто).

Теперь давайте установим наши основные модули.

// package.json
"dependencies": {
    "body-parser": "1.18.2",
    "express": "4.16.3",
    "mongoose": "5.0.16"
}

Express.js - это веб-фреймворк Node.js. Он сделает за нас всю тяжелую работу по обработке HTTP-запросов [POST, GET, PUT, DELETE], также известный как [CREATE, READ, UPDATE, DELETE]. Некоторым нравится начинать с чтения, но как прочитать то, что еще не создано?

Нам нужен body-parser для получения тела входящих HTTP-запросов POST. Есть ли способ сделать это без парсера тела? да. Мы могли бы написать это вообще без Express, но я недостаточно хорошо разбираюсь в этом, чтобы писать об этом. Помните, что эти модули - просто набор функций, которые кто-то написал. Вы можете написать свои собственные функции так же, как вы можете написать свою собственную веб-среду на C ++. Но что об этом скажет Sweet Brown?

Mongoose - это средство сопоставления документов объектов для MongoDB. Он обрабатывает приведение типов и проверку, которые нам понадобятся для нашего блога. Если хотите, можете воспользоваться экспресс-валидатором. Мы могли бы использовать его позже, но это будет шанс написать одну простую функцию вместо того, чтобы требовать другой пакет.

Вы можете увидеть несколько файлов package.jsons и заметить, что перед номерами версий стоят символы каратов (^) или тильды (~). Мы собираемся опустить их, чтобы наше приложение всегда использовало одну и ту же версию. Таким образом, если он работает сегодня, он будет работать завтра, точка

Давайте установим наши пакеты. Вернитесь в командную строку и введите

$ npm install

Это установит 334 пакета. Это звучит много, это так. Но это самый простой план, который я мог придумать. Чуть меньше 15 МБ.

Пришло время кодирования

В командной строке введите:

$ touch server.js

Вы также можете просто создать новый файл в своем редакторе, но это проще. Перейдите в свой редактор, откройте этот новый server.js и введите:

// server.js
console.log('Hello Node');

Мне нравится время от времени следить за тем, чтобы что-то работало. Начало похоже на хорошее время. Мы собираемся запустить наше новое приложение. Сохраните server.js, вернитесь в командную строку и введите

$ nodemon

Помните, как мы изменили точку входа при запуске npm init? Поскольку мы назвали его server.js, а файл, содержащий 'Hello Node' console.log (), называется server.js, Nodemon знает чтобы «ввести» наше приложение в этот файл. Nodemon все еще работает, ожидая изменений в нашем приложении. Вернитесь к server.js, добавьте еще один console.log () и сохраните файл. Проверьте свое командное окно.

Nodemon распознал, что мы изменили файл, и автоматически перезапустил наш сервер. Вы должны увидеть «Hello Node» и все, что написано в вашем новом console.log (). Если бы не сработало. Попробуйте снова. Не двигайтесь дальше, пока это не произойдет. Не забудьте удалить оба журнала консоли и оставить пустой server.js, прежде чем мы продолжим.

Требуются некоторые модули

Мы «установили» нужные нам модули. Но это просто означает, что мы загрузили некоторые файлы из npm на наш локальный компьютер. Наше приложение не знает, какие пакеты мы хотим использовать. На данный момент он ничего не знает. Давайте их включим. (Включить, потребовать, импортировать: указать одному файлу использовать другие файлы)

// server.js
const bodyParser = require('body-parser');
const express = require('express');
const mongoose = require('mongoose');

Теперь все функции этих трех модулей доступны для server.js. Но как нам добраться до самого API? Нам нужно настроить его так, чтобы мы могли переходить к нему через порт. Что такое порты?

Под запросами введите тип

// server.js
const app = express();
app.listen(8080, err => {
  if(err) console.error(err);
  console.log('Server listening on port 8080...');
});

Первая строка фактически создает (инициализирует) наше приложение, вызывая экспресс-функцию, которая нам нужна выше. Имя переменной ‘app’ является условным, и я настоятельно рекомендую вам следовать ему. Каждый учебник и ответ о переполнении стека будут использовать его, а также Экспресс-документы.

Следующий бит - это функция, которая прослушивает соединения на указанном нами порту. В данном случае 8080. После номера порта у нас есть функция обратного вызова. В этом руководстве я использую стрелочные функции ES6. Узнай их. Изучите ES6 в целом. Я расскажу об этом в одном из следующих постов.

В функции стрелки мы обрабатываем любые ошибки, которые могут возникнуть, записывая их в консоль. console.log () предназначен только для нашего душевного спокойствия. Приятно осознавать, что все работает. Сохраните свой server.js. Поскольку я никогда не говорил вам, как остановить nodemon, он все еще должен работать. Он должен был обнаружить изменения, которые мы только что внесли, и выглядеть так.

Управляйте своими маршрутами

Теперь мы собираемся остановить сервер, чтобы создать еще несколько файлов. В окне командной строки нажмите CTRL + C. Нет, он ничего не копирует в буфер обмена, но я гарантирую, что вы попытаетесь использовать его как таковой в будущем, и вам придется перезапустить сервер. Я все еще делаю. (Чтобы скопировать в Git Bash, нажмите CTRL + INS).

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

$ mkdir app
$ cd app
$ touch config.js
$ touch router.js
$ mkdir controllers
$ cd controllers
$ touch blogpost.controller.js
$ cd ..
$ mkdir models
$ cd models
$ touch blogpost.model.js
$ cd ../..
$ mkdir test
$ cd test
$ touch blogpost.test.js
$ cd ..

Что мы только что сделали? Мы создали каталог app, в котором будут храниться основы нашего приложения. В этом каталоге мы создали два файла: config.js и router.js. Мы также вложили в каталог приложения два каталога: контроллеры и модели. В каталоге controllers мы создали blogpost.controller.js. В каталоге models мы создали blogpost.model.js.

Наконец, мы создали тестовый каталог в корневом каталоге и создали в нем файл blogpost.test.js. Вы можете возразить, что тестовый каталог и файл не являются обязательными, и я бы сказал, что вы ошибаетесь.

Сначала тестируем. Тестируем часто.

Убедитесь, что вы вернулись в корневой каталог, и снова запустите сервер. Помните как? В противном случае введите «nodemon».

config.js

Посмотрите на метод app.listen () в server.js. Вы заметили что-то уродливое? Что-то не так'?

Мы жестко запрограммировали номера портов. Это табу в разработке. Технически это не так. Я имею в виду, эй, это работает правильно. Но если мы просто собираемся жестко кодировать данные, тогда зачем мы вообще создаем API? Почему бы не закодировать наши сообщения в HTML-файлах?

Потому что это неэффективно и небрежно.

Я не хочу поддерживать неограниченное количество файлов HTML. Я бы предпочел скачать более 3600 файлов с npmjs.com и написать сотню строк кода, чтобы в будущем я мог сесть на пляже, открыть форму и опубликовать сообщение в блоге за считанные минуты.

В файле config.js мы будем хранить номер нашего порта. Мы могли бы просто сделать его переменной в server.js, но в будущем мы, возможно, захотим добавить ряд других параметров конфигурации. Если / когда наступит этот день, у нас будет хорошее место для этого.

Давайте откроем config.js, создадим объект и экспортируем его. Предупреждение о триггере: слишком много кода для наших целей, но я предполагаю, что вы планируете делать больше, чем писать один простой API.

// config.js
const config = {
  port: process.env.PORT || 8080,
  db: 'mongodb://localhost/myblog',
  test_port: 4242,
  test_db: 'mongodb://localhost/myblog_test'
}
module.exports = config;

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

А как насчет переменной «process.env.PORT». Это когда мы размещаем наш API позже на AWS (или Google, Heroku и т. Д.). Где-то в головокружительном множестве настроек, которые вы выбираете при запуске приложения, вы можете установить переменные среды. Одна из этих переменных - это порт, который вы хотите использовать. Если вы не установите этот порт, наше приложение будет "по умолчанию" на 8080. "||" означает "ИЛИ" в JavaScript, но вы это уже знали.

«Db» и «test_db» - это расположение нашего локального экземпляра MongoDB. Вы правильно установили MongoDB? В следующем руководстве мы подключимся к облачной службе Atlas MongoDB (URL-адрес буквально… / cloud / atlas, мы, разработчики, не такие уж мрачные). А пока будем делать все локально. Нам нужно использовать отдельную базу данных для наших тестов, потому что наши тесты будут полностью очищать базу данных каждый раз, когда мы запускаем тест.

Наконец, мы экспортируем объект как модуль, который будет использоваться в server.js.

Говоря о server.js, давайте вернемся к нему и переделаем метод app.listen (), а также импортируем только что созданный модуль конфигурации.

// server.js
const config = require('./app/config');
app.set('port', config.port);
app.listen(app.get('port'), err=>{
  if(err) console.error(err);
  console.log(`Server listening on port ${app.get('port')}...`);
});

Сначала мы импортируем наш модуль конфигурации. Вы могли заметить, что мы не включили «.js» в имя файла конфигурации. Вы не обязаны. Node.js знает, что вы имеете в виду.

Мы устанавливаем порт приложения на порт из config.js. Нам не нужно было называть его «порт». Мы могли бы назвать его ‘thePortIWantToUseForMyApp’. Экспрессу все равно. Узлу все равно. Но мне все равно. Я действительно так делаю. Так что давайте просто назовем наш порт «порт».

Вы увидите, что в app.listen () мы «получаем» только что «настроенный» порт. Мы могли бы просто использовать здесь config.port, но тогда вы бы не узнали о app.set (). Посмотрите на console.log (). Обратите внимание, что мы больше не используем одинарные кавычки. Мы перешли на обратные кавычки, чтобы реализовать литералы шаблонов ES6. Мы использовали знак доллара, за которым следовал наш app.get () в фигурных скобках. Это будет работать для любой переменной. Примечание. Это не сработает в каждом браузере, поскольку внедрение ES6 не является универсальным, но мы же не в браузере, не так ли?

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

blogpost.model.js

Как и почти любой файл или переменная в этом руководстве, вы можете использовать любые имена, какие захотите. ".Model" совершенно излишне. Это даже избыточно. Ради всего святого, этот файл находится в каталоге моделей. Но использование этого соглашения об именах устраняет всякую двусмысленность в отношении того, какой тип файла вы будете редактировать в будущем. Представьте себе будущее, в котором у вас будет дюжина каталогов и сотни файлов. Тогда ты меня поблагодаришь. Откройте его и введите это

// blogpost.model.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// BlogPost Schema
const BlogPostSchema = mongoose.Schema({
  url: {
    type: String,
    required: true,
    unique: true
  },
  title: {
    type: String,
    required: true
  },
  body: {
    type: String,
    required: true
  },
  date: {
    type: Date,
    default: Date.now
  },
  tags: [String],
  updated: {
    type: Date
  }
});
module.exports = mongoose.model('BlogPost', BlogPostSchema);

Сначала мы импортируем mongoose, а затем инициализируем конструктор mongoose.Schema как «Schema». С помощью этого конструктора мы будем «моделировать» или определять формат наших документов MongoDB. Эти первые две строки будут одинаковыми в любом файле модели.

Следующее объявление - это создание новой модели. Я постарался сделать его максимально простым, поэтому мы увидим только два разных типа данных: String и Date. Это собственные типы JavaScript. Обратите внимание, что некоторые из них требуются, а некоторые нет, это поможет в нашей проверке позже.

Мы также сделали "url" уникальным. Это потому, что мы не хотим путаницы, когда кто-то создает постоянную ссылку или вручную вводит URL-адрес записи блога, которая ему нравится и которую он хочет проверить позже. По умолчанию мы установили "дату" (в данном случае это дата публикации сообщения) на Date.now. Это означает, что всякий раз, когда мы добавляем сообщение в блог в базу данных, текущая временная метка UNIX будет сохраняться в нашей базе данных. «Теги» - это массив строк. Мы сможем добавить столько, сколько захотим, если их не будет миллионами, и в этом случае это приведет к превышению лимита документа в 16 МБ.

Последняя строка выполняет две функции. Правая половина знака равенства создает копию BlogPostSchema, и мы сообщаем Mongoose, что коллекция MongoDB, в которой мы хотим хранить сообщения блога, называется BlogPosts. Название коллекции будет во множественном числе, но мы даем ему имя в единственном числе, которое мы будем использовать для создания новых сообщений в блоге. Он обрабатывает интерпретацию автоматически. Левая часть знака равенства экспортирует нашу модель в остальную часть приложения. Это объект Node.js.

blogpost.controller.js

Теперь у нас есть модель для всех сообщений блога, которые будут добавлены в нашу базу данных MongoDB. Мы знаем, что все сообщения будут иметь одинаковый формат, поэтому, когда мы пишем интерфейс, мы можем быть уверены, что независимо от того, какой пост мы потянем для отображения на нашем сайте, он будет настроен одинаково. У каждого сообщения обязательно будет URL-адрес, заголовок, тело и дата. Они могут иметь или не иметь тегов. Сообщения могут никогда не обновляться, но если они обновляются, у нас есть место для хранения времени, когда это произошло.

Теперь нам нужно определить инструкции того, что должен делать наш API, когда мы отправляем ему данные из браузера (или теста, или почтальона). Вот код. Позже мы добавим больше для различных операций CRUD, а пока мы просто хотим отправлять сообщения POST (HTTP-глагол) (сообщения в блогах).

// blogpost.controller.js
"use strict"
const BlogPost = require('../models/blogpost.model');
// create a new Blog Post
exports.publishPost = (req, res) => {
  const NewBlogPost = new BlogPost(req.body);
  NewBlogPost.save((err, blogPost) => {
    if(err) {
      return res.status(422).json({
        msg: 'Server encountered an error publishing blog post.',
        error: err
      });
    }
    else {
      return res.status(200).json({
        msg: 'Successfully published blog post.',
        blogPost: blogPost
      });
    }
  });
};

Начнем с выражения «используйте строгое». Это говорит нашему приложению работать в строгом режиме. По сути, это не позволяет нам делать определенные вещи, такие как использование необъявленных переменных, дублирование имен параметров в определениях функций и дублирование имени свойства в литерале объекта. Не то чтобы я позволил вам совершить какую-либо из этих ошибок, но бывают случаи, когда вы хотите использовать этот режим. Это как-раз тот случай. Контроллер, пожалуй, самая важная часть нашего приложения. Он определяет все поведение для наших операций CRUD.

Затем мы импортируем нашу модель.

Затем мы определяем функцию, которая будет немедленно экспортирована. «Req» и ​​«res» - это соглашения в Node.js для представления объектов запроса и ответа. Вы можете переименовать их, но зачем путать себя в будущем ради противоречия.

Запрос - это данные, поступающие от пользователя (GET, POST и т. д.)

Ответ - это то, что веб-служба отправит обратно

Все данные о новом посте будут в теле объекта запроса. Express превращает это в аккуратный объект с именем req.body. Мы назначаем это новому объекту с именем NewBlogPost. Мы создаем это новое сообщение в блоге, используя модель «BlogPost», которую мы только что импортировали в верхней части скрипта.

Все остальное - одна функция. Это метод Model.save (). Мы собираемся передать только один аргумент, функцию обратного вызова, чтобы сообщить нам, что произошло, когда был вызван метод .save (). Первая ветвь оператора if сообщает нам, что произойдет, если при сохранении нового BlogPost в базе данных произошла ошибка. Вторая - это ветвь успеха. Мы должны передать этому обратному вызову два аргумента. Первый - «err», объект ошибки в случае, если что-то пойдет не так. Второй - это «blogPost», который представляет собой объект, который будет содержать сохраненный blogPost.

В случае сбоя .save () функция обратного вызова вернет код состояния HTTP 422. Этот код сообщает нам, что у нас есть Необработанная сущность. Этот код сообщает нам, что запрос был правильно сформирован, но не удалось выполнить из-за семантических ошибок. Мы прикрепляем это к объекту ответа с помощью res.status (422). Затем мы подключаемся к JSON для получения более подробного и понятного ответа от сервера. Мы собираемся отправить объект с двумя свойствами: первое - это сообщение (msg), чтобы позволить себе (или другому будущему разработчику, это не все о вас) знать, что, когда не так, на простом английском языке. Второе свойство (error) имеет значение err, которое является объектом ошибки, отправленным обратно из нашего приложения. Когда мы тестируем наше приложение, мы увидим некоторые объекты ошибок от Mongoose.

В случае успеха .save () мы отвечаем статусом успеха «200» и некоторым количеством JSON. Аналогичная настройка, за исключением того, что вместо отправки ошибки (поскольку ее не было) мы возвращаем объект blogPost, который только что был сохранен в базе данных.

router.js

Наше приложение теперь знает, что делать, когда оно получает новый blogPost (контроллер), и знает, как оно должно выглядеть (модель). Но он еще не знает, когда это делать. Для этого нам нужно определить несколько маршрутов. Вот код для router.js.

// router.js
const express = require('express');
const BlogController = require('./controllers/blogpost.controller');
module.exports = app => {
  // route groups
  const apiRoutes = express.Router();
  const blogPostRoutes = express.Router();
  // middleware for apiRoutes
  apiRoutes.use('/blogPosts', blogPostRoutes);
  //// Blog Post Routes //////////////
  // POST a new blog post
  blogPostRoutes.post('/', BlogController.publishPost);
  // url for all API routes
  app.use('/api', apiRoutes);
};

Сначала нам нужно импортировать экспресс. Нам нужно сделать это, чтобы мы могли вызвать метод .Router () для создания двух новых объектов маршрутизатора всего за немного времени. Затем мы импортируем только что созданный модуль контроллера.

Остальной код - это одна функция. Мы передаем app в качестве аргумента, чтобы мы могли прикрепить наши маршруты в конце. Мы также экспортируем эту функцию для использования в server.js.

Мы создали две группы маршрутов. Один будет нашим всеобъемлющим apiRoutes. Для этого урока у нас будут только сообщения в блогах, так что это необязательный шаг. Но подумайте о будущем, в котором вы хотите сохранять информацию о людях, которые посещают ваш сайт, или их комментариях. Они подпадут под ваш API, но не будут сообщениями в блогах. Это наша следующая группа, blogPostRoutes.

Теперь нам нужно настроить промежуточное ПО для наших apiRoutes. Нам просто нужно сказать ему, что делать при обнаружении маршрута. Если вы посмотрите на последнюю строку кода, то увидите, что мы сообщаем самому приложению, что делать, когда запрос приходит на «/ api». Он должен использовать промежуточное ПО apiRoutes. Поэтому всякий раз, когда приходит запрос на «/ api / blogPosts», наше приложение будет использовать все функции, которые мы прикрепляем к blogPostRoutes.

Сейчас мы собираемся определить поведение только для одного HTTP-глагола для blogPostRoutes, POST. Поэтому всякий раз, когда на / api / blogposts / поступает HTTP-запрос POST, наше приложение будет использовать функцию BlogController.publishPost (). Мы просто написали эту функцию в контроллере в предыдущем разделе.

Вернуться к server.js

Я собираюсь поделиться всем server.js, а не только строками, которые вам нужно добавить. Я рекомендую вам идти строка за строкой и добавлять или изменять каждую строку по мере ее появления, а не просто копировать и вставлять. Если вы хотите скопировать и вставить, просто клонируйте репо. Примечание. Узлу не понравится, когда вы сохраните этот файл, потому что MongoDB не работает. На следующем этапе мы запустим MongoDB.

// server.js
const bodyParser = require('body-parser');
const express = require('express');
const mongoose = require('mongoose');
const config = require('./app/config');
const router = require('./app/router');
const app = express();
app.use(bodyParser.json());
app.set('port', config.port);
app.listen(app.get('port'), err=>{
  if(err) console.error(err);
  console.log(`Server listening on port ${app.get('port')}...`);
  const db = mongoose.connect(config.db);
  mongoose.connection.on('connected', () => {
    console.log(`Mongoose connected to ${config.db}`);
  });
});
router(app);

Первые добавления - это импорт для config.js и router.js. Теперь наше приложение полностью осведомлено о функциях, которые мы только что написали, и мы можем использовать их по желанию в server.js.

Затем мы говорим приложению использовать bodyParser. Нам это нужно для того, чтобы приложение могло анализировать req.body в функции контроллера .publishPost (). Я не знаю, почему это не просто экспресс. Думаю, раньше было. Мы используем метод .json () для анализа запросов JSON. Наш интерфейс не будет отправлять данные прямо из формы (я где-то читал, что это уже не модно). Мы собираемся отправить его в формате JSON.

Теперь заглянем внутрь нашего метода app.listen () после console.log, где мы объявляем, что сервер слушает. Здесь мы фактически подключаемся к базе данных через mongoose.connect (). Он обрабатывает все мелочи на заднем плане. Затем мы вызываем метод .on (), чтобы сообщить нам, когда база данных подключена.

Если по какой-то причине ваш сервер не работает, введите в командной строке «nodemon», и вы должны это увидеть.

Это было бы хорошее время, чтобы добавить немного кода. Нажмите CTRL + C, чтобы остановить сервер и вернуть запрос.

Нам не нужно размещать все эти модули узлов в нашем репо. Добавьте новый файл в корневой каталог как таковой:

touch .gitignore

Откройте этот только что созданный файл и добавьте эту единственную строку.

node_modules/

Это скажет git игнорировать этот каталог и все его содержимое. Теперь введите эти команды.

$ git add .
$ git commit -m "POST route for blogPosts written but not tested"
$ git push -u origin master

Время для теста

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

$ cd "c:\program files\mongodb\server\3.6\bin"

Затем введите:

$ mongod

Теперь у вас есть MongoDB. Вы можете свернуть это новое окно оболочки.

Теперь, когда у нас есть один полностью сформированный маршрут, нам нужно протестировать его, чтобы убедиться, что он работает должным образом. Сначала нам нужно изменить наш server.js, чтобы мы использовали наши test_port и test_db.

// server.js
if(process.env.NODE_ENV === "test") {
  app.set('port', config.test_port);
  app.listen(app.get('port'), err => {
    if(err) console.error(err);
    console.log(`Server listening on port$ {app.get('port')}...`);
    const db = mongoose.connect(config.test_db);
  });
} else {
  app.set('port', config.port);
  app.listen(app.get('port'), err => {
    if(err) console.error(err);
    console.log(`Server listening on port ${app.get('port')}...`);
    const db = mongoose.connect(config.db);
    mongoose.connection.on('connected', () => {
      console.log(`Mongoose connected to ${config.db}`);
    });
  });
}

Мы настроили простой оператор if, чтобы проверить, запускаем ли мы тесты или нет. Если мы запускаем тесты, мы хотим использовать наши тестовые переменные. Если мы находимся в «живом» (или «производственном») режиме, мы хотим использовать обычные переменные порта и db. Мы установим эту переменную среды для тестирования на следующем шаге.

blogpost.test.js

Давайте разделим это на две части для усвояемости: установка и сам тест.

// blogpost.test.js
process.env.NODE_ENV = 'test';
const BlogPost = require('../app/models/blogpost.model');
const server = require('../server');
const chai = require('chai');
const expect = chai.expect;
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
const blogURL = '/api/blogposts';

Как я только что обещал, мы установили для переменной среды Node значение ‘test’.

Далее нам потребовалось несколько вещей. Сначала мы импортируем модель BlogPost, чтобы мы могли использовать методы из mongoose.model. А именно .remove (). Затем мы импортируем нашего старого друга server.js. Напомните мне вернуться туда и добавить небольшую строку кода, чтобы эта строка работала.

Следующие четыре строки предназначены для нашей среды тестирования. И последняя строка - это URL-адрес нашего API. Тот, который мы установили в router.js. Вот и сам тест.

// blogpost.test.js
describe('Blog Posts', () => {
  beforeEach(done => {
    BlogPost.remove({}, err => {
      if(err) console.error(err);
      done();
    });
  });
  describe('/POST/ - publish a BlogPost', () => {
    it('it should POST a new BlogPost', done => {
      const NewBlogPost = {
        url: 'my-first-blog-post',
        title: 'My First Blog Post',
        body: 'This is some text. Lorem ipsum... etc...',
        tags: ['blog', 'nodejs', 'api']
      };
      chai.request(server)
          .post(blogURL)
          .send(NewBlogPost)
          .end((err, res) => {
            expect(res).to.have.status(200);
            expect(res.body).to.be.an('object');
            expect(res.body).to.have.property('msg')
              .eql('Successfully published blog post.');
            expect(res.body).to.have.property('blogPost');
            expect(res.body.blogPost).to.have.property('url').and.be.an('string').eql(NewBlogPost.url);
            expect(res.body.blogPost).to.have.property('title').and.be.an('string').eql(NewBlogPost.title);
            expect(res.body.blogPost).to.have.property('body').and.be.an('string').eql(NewBlogPost.body);
            expect(res.body.blogPost).to.have.property('tags').and.be.an('array').and.have.length(NewBlogPost.tags.length).eql(NewBlogPost.tags);
            expect(res.body.blogPost).to.have.property('date');
            expect(res.body.blogPost).to.have.property('_id');          done();
          });
    });
  });
});

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

beforeEach () выполняется перед каждым тестом в этом блоке кода. Не путать с функцией before (), которая выполняется только один раз перед всем блоком. Итак, перед каждым тестом мы хотим удалить все сообщения блога из базы данных. Теперь вы понимаете, почему нам пришлось установить тестовый порт и базу данных. Было бы обидно, если бы мы опубликовали несколько десятков сообщений, а затем решили протестировать некоторые новые функции и стереть нашу производственную базу данных. BlogPost.remove () - это метод мангуста.

Далее идет вложенный блок description. Это сам тест. it сообщает нам, что мы действительно начинаем тест.

Нам нужны данные для отправки в наш API, поэтому мы создаем новый объект с именем «NewBlogPost» с некоторыми примерами данных, которые представляют собой типичное сообщение в блоге.

Теперь мы фактически выполняем HTTP-запрос. Мы должны использовать наш модуль server.js, потому что это наше приложение. Затем мы говорим Mocha выполнить запрос POST. Затем мы сообщаем ему, что отправить, в данном случае объект ‘NewBlogPost’, который мы только что создали.

Теперь мы .end () тестируем тело ответа из нашего API. Итак, что мы ожидаем от ответа. Я запишу тело ответа, чтобы вы могли сами увидеть, как он выглядит.

Это тело ответа - это то, на чем мы будем запускать наши тесты expect (). Сначала мы тестируем сам ответ (а не тело), ​​чтобы убедиться, что мы получили желаемый код HTTP-ответа 200. Затем мы убеждаемся, что res.body является объектом, у этого объекта есть свойство ' msg ', и это сообщение мы указали в blogpost.controller.js.

Затем мы проверяем свойство «blogPost». Следующие четыре expect () легко предсказать. Мы проверяем, имеет ли наш res.body четыре свойства, которые мы отправили в запросе. Мы проверяем их типы и проверяем, соответствуют ли они тому, что мы указали выше. Мы также проверяем длину массива «теги».

Следующие два свойства, для которых мы проверяем их существование, были автоматически созданы нашим приложением. Свойство date мы устанавливаем по умолчанию в нашей модели. ‘_id’ автоматически устанавливается самой MongoDB.

Наконец, мы done () с нашим тестом.

Еще одна вещь, которую нам нужно сделать, чтобы запустить наш тест. Нам нужно, чтобы наше приложение знало, что делать, когда мы запускаем тесты. Перейдите в package.json и найдите свойство scripts. Значением должен быть объект с одним свойством «тест». Удалите значение свойства «test» и замените следующим значением

// package.json
// replace this
“test”: “echo \”Error: no test specified\” && exit 1"
// with this
"test": “mocha --timeout 9999 --exit”

Mocha - это наша среда тестирования. Мы установили для теста таймаут 9999 миллисекунд (почти 10 секунд). И, наконец, мы говорим ему выйти из нашего приложения или прекратить прослушивание любых портов, как только оно завершит все тесты.

Мы должны быть готовы. Давай попробуем. Остановите сервер, если он работает, и введите.

$ npm test

Что пошло не так? Вы видите, что TypeError: app.address не является функцией? Помните, я говорил вам напомнить мне добавить строку кода в server.js? Ну ты забыл. Вернемся к server.js и добавим эту строку в самый низ файла.

// needed for testing porpoises only
module.exports = app;

Нам не удалось импортировать server.js в наш тест, потому что мы никогда его не экспортировали. На производстве нам этого делать не нужно. Теперь вернитесь и снова запустите тест. Вот скриншот того, что вы должны увидеть:

Нам нужно протестировать нашу ветку отказа

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

Вернитесь к blogpost.test.js и сразу же после блока it и прямо перед закрывающей фигурной скобкой "/ POST /" описания блога введите этот код.

it('it should not POST a new BlogPost without a body', done => {
      const NewBlogPost = {
        url: 'my-first-blog-post',
        title: 'My First Blog Post',
        tags: ['blog', 'nodejs', 'api']
      };
      chai.request(server)
          .post(blogURL)
          .send(NewBlogPost)
          .end((err, res) => {
            expect(res).to.have.status(422);
            expect(res.body).to.be.an('object');
            expect(res.body).to.have.property('msg')
              .eql('Server encountered an error publishing blog post.');
            expect(res.body).to.have.property('error').and.be.an('object');
            expect(res.body.error).to.have.property('errors')
              .and.be.an('object');
            expect(res.body.error.errors).to.have.property('body')
              .and.be.an('object');
            expect(res.body.error.errors.body).to.have.property('kind')
              .eql('required');
          done();
          });
    });

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

Мы проверяем код состояния (500) и ошибку «msg», установленную в blogpost.controller.js. Затем мы проверяем установленное нами свойство «error», которое содержит свойство «errors» из Mongoose. Ожидается, что этот объект errors сообщит нам, какое свойство мы забыли включить. Это свойство также является объектом, который сообщает нам, какой «вид» ошибки мы вызвали. В данном случае это ошибка «required», поскольку свойство ‘body’ является обязательным элементом BlogPost.

Если бы нам не требовалось тело в нашей модели, этот тест провалился бы. И нет, двойное отрицание в данном случае не равно положительному. Попробуйте сами. Зайдите в свой blogpost.model.js и удалите «required: true» из поля «body». (Не забудьте убрать запятую после String, иначе вам будет плохо.) Запустите тест еще раз. У вас должен быть 1 пас и 1 провал. Наше приложение отправит код состояния 200, потому что оно успешно добавило это сообщение блога без основного текста в базу данных.

Не забудьте вернуться и снова добавить в модель required: true (и запятую), прежде чем продолжить.

Внедрение новых функций

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

Мне не нравится, что мы сами создаем URL. Я лучше просто отправлю заголовок, текст и теги, и пусть приложение создаст для нас URL. Давайте добавим эту функцию и протестируем ее. Откройте blogpost.test.js и внесите эти изменения. Сначала удалите свойство и значение url из объекта NewBlogPost. Я просто закомментировал это, чтобы вы могли видеть, что именно нужно удалить.

// blogpost.test.js
const NewBlogPost = {
  // url: 'my-first-blog-post', // you can remove this line
  title: 'My First Blog Post',
  body: 'This is some text. Lorem ipsum... etc...',
 tags: ['blog', 'nodejs', 'api']
};

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

// blogpost.test.js
const theURL = NewBlogPost.title.toLowerCase().split(' ').join('-');

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

Теперь нам нужно переделать сам тест. В строке, где мы проверяем URL-адрес, нам нужно изменить строку, с которой мы сравниваем ответ, с «NewBlogPost.url» (так как он давно не существует) на «theUrl» (переменная, которую мы только что определили).

// blogpost.test.js
// change this
expect(res.body.blogPost).to.have.property('url')
  .and.be.an('string').eql(NewBlogPost.url);
// to this 
expect(res.body.blogPost).to.have.property('url')
  .and.be.an('string').eql(theURL);

Примечание. Удалите свойство ‘url’ из NewBlogPost во втором тесте, так как мы больше не будем отправлять URL-адрес с нашими HTTP-запросами. У вас нет , но если вы этого не сделаете, проверка на ошибку не будет на 100% точной. Вы будете проверять, отправил ли кто-нибудь запрос с полем URL, чего никогда не произойдет.

Откройте blogpost.controller.js и введите эти две строки в самом начале метода publishPost ().

const theURL = req.body.title.toLowerCase().split(' ').join('-');
req.body['url'] = theURL;

Это точно такая же манипуляция со строкой, которую мы выполняли в нашем тесте, с той лишь разницей, что мы манипулируем req.body.title. Вторая строка присоединяет эту новую строку к свойству ‘url’ объекта req.body.

Вернитесь и протестируйте npm свое приложение. Оба теста должны пройти успешно.

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

Наконец, введите свой код.

Заворачивать

Теперь у нас есть API Node.js, на который мы можем отправлять запросы POST, которые добавят сообщение в блог в коллекцию базы данных MongoDB. У нас также есть автоматизированные тесты, которые определяют, что произойдет, если запрос POST будет успешным или неудачным.

Затем мы настроим и протестируем остальные три операции CRUD. Мы сможем ПОЛУЧАТЬ сообщения блога для отображения в нашем блоге, ОБНОВЛЯТЬ эти сообщения новыми данными и УДАЛЯТЬ сообщения, если они нам больше не нужны.

Вот ссылка на репо на GitHub.

Изучите JavaScript с моими решениями задач Check.io

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