click fraud detection
click fraud detection
Blog Case

IM на основе NodeJS Express и Angular 7. Часть 3

BLOG
CASE
204
0
0/ 5stars
0/5
Время чтения: 30 минут

Часть 1. Серверная

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

Где мы имеем 4 таблицы с категориями и подкатегориями, товарами и изображениями. Все таблицы связаны друг с другом внешними ключами.

Наша задача заключается в том, чтобы создать RESTfull API сервис (серверная часть), используя NodeJS для возможности вывода каталога товаров в одностраничное приложение на Angular (клиентская часть).

Будем использовать СУБД PoptgresSQL.

Начнем с серверной части.

RESTfull API cервер на NodeJS и Express

Инструментарий

Прежде всего настроим наше рабочее окружение и сделаем максимально удобным процесс разработки и отладки.

Для установки NodeJS и приведения его в актуальное состояние в OC  на основе Debian выполним следующую последовательность команд:

sudo apt install nodejs
sudo npm cache clean -f
sudo npm install -g n
sudo n stable

Проверяем установленную версию.

node -v

В данный момент у меня установлена версия 10.15.3.

Создадим каталог с будущим сервером и инициализируем в нем пакет npm (package.json).

mkdir server
cd server
npm init

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

npm install --save nodemon

Так как мы будем использовать Typescript, установим его.

npm install --save-dev typescript

Установим остальные инструменты, которые будем использовать в работе.

npm install express --save
npm install --save-dev @types/node ts-node

express – фреймворк для построения REST API

@types/node – плагин для разрешения типов для nodejs

ts-node – позволит запускать typescript файлы без транспиляции их в нативный javascript

Также установим типы и для express.

npm install @types/express --save

Создаем файл tsconfig.json c настройками транспилятора typescript.

{
    "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "dist",
    "sourceMap": true
    },
    "include": [
    "src/**/*.ts"
    ],
    "exclude": [
    "node_modules",
    ".vscode"
    ]
}

Так как мы указали папку src в качестве исходной, добавим туда файл main.ts с кодом простейшего сервера.

import * as express from "express";
const app = express();
app.get("/", (req, res) => {
    res.send("Hello World")
})
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
     console.log(`Server is running in http://localhost:${PORT}`)
})

Пропишем команды для запуска сервера в секции script файла package.json.

"scripts": {
  "start": "node --inspect=5858 -r ts-node/register ./src/main.ts",
  "start:watch": "nodemon",
  "build": "tsc"
},

Разберем команду start, которая запускает сервер.

— inspect=5858 – включает порт 5858 для дебагинга проекта;

-r ts-node/register ./src/server.ts – запускает typescript файл где ts-node/register позволяет его транспилировать на лету.

“start”: “nodemon” – стартует процесс nodemon, отслеживающий изменения кода.

Осталось добавить настройки для nodemon в секцию nodemonConfig файла package.json.

"nodemonConfig": {
  "ignore": [
  "**/*.test.ts",
  "**/*.spec.ts",
  ".git",
  "node_modules"
  ],
  "watch": [
  "src"
  ],
  "exec": "npm start",
  "ext": "ts"
  }

Теперь можно запустить сервер командой. 

npm start:watch

Для того, чтобы включить режим дебага в редакторе VS Code необходимо создать файл launch.json в каталоге .vscode со следующим содержимым где добавить процесс nodejs в редактор.

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Node: Nodemon",
      "processId": "${command:PickProcess}",
      "restart": true,
      "protocol": "inspector"
    }
  ]
}

Теперь при нажатии значка дебага в левом верхнем углу и выбора соответствующего пункта в списке процессов автоматически начнется отладка и при установке брекпоинта в коде вас автоматом перебросит в редактор.

Вы также можете отлаживать код прямо в браузере chrome. Для этого введите в адресную строку chrome://inspect и сконфигурируйте настройки искомых сетевых устройств, добавив туда localhost:5858.

После чего ваш сервер станет доступен в списке Remote target и вы можете запустить отладку, нажав на ссылку Inspect.

Работа с базой данных PostgreSQL.

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

Для работы с базой данных мы будем использовать пакет node-postgres, который установим командой

npm install --save pg

Далее, создадим соединение передав его параметры объекту Pool. Если их не передавать, он попытается их взять из переменных окружения, например PGHOST, PGDATABASE  и т.д. что рекомендуется делать дабы избежать хардкода в проекте и случайно не выложить ваши секреты в открытый доступ.

const Pool = require('pg').Pool
const pool = new Pool({
  user: 'postgres',
  host: 'localhost',
  database: 'curtains',
  password: '****',
  port: 5432,
})

Далее мы импортируем нужные типы express и задействуем объект pool для выборки данных в главном роутинге.

import {Request, Response} from "express";
...
app.get("/", (req: Request, res: Response) => {
    pool.query('SELECT * FROM shop_category ORDER BY id ASC', (error, results) => {
        if (error) {
          throw error
        }
        res.status(200).json(results.rows)
      })
})

В результате на главной странице мы получим следующую картину:

Этот запрос можно переписать более короче (без колбеков), используя await/async.

app.get("/", async (req: Request, res: Response) => {
    const result = await pool.query('SELECT * FROM shop_category ORDER BY id ASC')
    res.status(200).json(result.rows)
})

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

app.get("/", async (req: Request, res: Response) => {
    const result = await pool.query('SELECT * FROM shop_category ORDER BY id ASC')
    for(let category of result.rows){
        const subcats = await pool.query(`SELECT * FROM shop_subcategory where category_id=${category.id}`)
        category.subcategories = subcats.rows;
    }
    res.status(200).json(result.rows)
})

Так как держать всю логику приложения в одном файле main.ts не совсем правильно и удобно, вынесем API, связанное с категориями в отдельный файл api/category.ts.

Так будет выглядеть заготовка для будущего класса RESTfull API сервиса:

import {Request, Response} from "express";

export class CategoryAPI {

    put(req: Request, res: Response){}

    getAll(req: Request, res: Response){}

    getOne(req: Request, res: Response){}

    post(req: Request, res: Response){}

    delete(req: Request, res: Response){}
}

Теперь можно резко сократить код в main.ts, вынести всю логику работы с базой данных в класс CategoryAPI и подключить его методы следующим образом.

import { CategoryAPI } from './api/category';

const category_api = new CategoryAPI();
app.get("/category/all", category_api.getAll);
app.get("/category/one/:id", category_api.getOne);
app.delete("/category/delete/:id", category_api.delete);
app.post("/category/edit", category_api.post);
app.put("/category/create", category_api.put);

Приведу пример вынесенного метода получения категорий (файл api/category.ts)

export class CategoryAPI {
    put(req: Request, res: Response){}

    async getAll(req: Request, res: Response){
        const result = await pool.query('SELECT * FROM shop_category ORDER BY id ASC')
        for(let category of result.rows){
            console.log(category);
            const subcats = await pool.query(`SELECT * FROM shop_subcategory where category_id=${category.id}`)
            category.subcategories = subcats.rows;
        }
        res.status(200).json(result.rows)        
    }

    getOne(req: Request, res: Response){}

    post(req: Request, res: Response){}

    delete(req: Request, res: Response){}
}

Тестирование.

Теперь разберемся с тестированием нашего приложения, как неотъемлемой части любого приложения средней и высокой сложности. Под тестированием в данном случае будем понимать генерацию ряда HTTP запросов на url-ы сервера разными методами (GET POST и т.д.) и проверку результатов их выполнения. Эти тесты запускаются из консоли и выдают результат туда же. Для создания тестов будем использовать 3 библиотеки.

Jasmine – простой фреймворк для написания логики тестов, который их поочередно запускает и генерирует отчет.

Устанавливаем его глобально или локально командами:

npm install jasmine-core jasmine @types/jasmine jasmine-ts --save-dev

Request – библиотека для тестирования HTTP запросов.

npm install --save-dev request @types/request

После установки инициируем папку spec c настройками jasmine командой

npm test init

Изменим расширение файлов-тестов c js на ts в spec/support/jasmine.json

{
  "spec_dir": "spec",
  "spec_files": [
    "**/*[sS]pec.ts"
  ],
  "helpers": [
    "helpers/**/*.ts"
  ],
  "stopSpecOnExpectationFailure": false,
  "random": true
}

Добавим в tsconfig.json

"allowSyntheticDefaultImports": true

Для того чтобы иметь возможность импортировать библиотеку jasmine конструкцией

import jasmine from 'jasmine';

Добавим команду test в секцию scripts файла package.json

"scripts": {
  ...
  "test": "./node_modules/.bin/jasmine-ts"
},

Пишем первый тест в файле spec/category.spec.ts на проверку статуса ответа сервера на 2 url-а.

import jasmine from 'jasmine';
import * as request from "request";

var base_url = "http://localhost:3000/"

describe("Category API test", function() {

    it("All categories code 200", function(done) {
        request.get(base_url+'category/all', function(error, response, body) {
        expect(response.statusCode).toBe(200);
        done();
      });
    });

    it("Add category code 200", function(done) {
      request.post(base_url+'category/create',{}, function(error, response, body) {
      expect(response.statusCode).toBe(200);
      done();
    });
  });    

});

При успешном прохождении тестов командой npm run test мы должны видеть:

2 specs, 0 failures
Finished in 0.075 seconds

При не успешном.

Пример проверки post запроса на создание категории и передачи данных в формате json.

it("Add category OK", function(done) {
    let cat: any = {'name': 'Test category', 'name_slug': 'test-category' }
    request.post({
    url: base_url+'category/create',
    body: cat,
     son: true
    }, function(error, response, body) {
    expect(JSON.parse(body)).toEqual({status: 0, message: 'Ok'});
    done();
  });
});

Сама функция создания категории, удовлетворяющая условию теста и возвращающая json объект {status: 0, message: 'Ok'}.

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

Установка.

npm install body-parser @types/body-parser --save

Приминение.

const app = express();
import * as bodyParser from 'body-parser';
app.use(bodyParser.json());

Полный код метода добавления новой категории.

async post(req: Request, res: Response){
    const sql = 'INSERT INTO shop_subcategory(name, name_slug) VALUES($1, $2)
RETURNING *'
    const values = [req.body.name, req.body.name_slug]

    try {
        const result = await pool.query(sql, values)
        res.status(200).json({status: 0, message: 'Ok'})
      } catch (err) {
        console.log(err.stack)
      res.status(200).json({status: 1, message: 'Error!'})
    }

}

Так как мы использовали стиль async/away, мы должны заключать запрос к базе данных внутрь конструкции try/catch.

Особое внимание следует уделить формированию json ответа, описывающее товар. Проблема заключается в том, что формат описания товара может со временем меняться. К нему могут добавляться новые поля и массивы, рейтинги, комментарии и пр. Поэтому процесс формирования json (серелизация) информации о товаре удобней всего хранить в каком-то одном месте, которое задействуется при создании объекта товара. Если мы создадим класс товара, то конструктор в данном случае не подойдет, т.к. в нем мы будем использовать асинхронные операции await/async и конструктор не может возвратить промис. Выход из этой ситуации видется автору в создании статичного фабричного async метода класса товара, котрый и будет выполнять все необходимые запросы к базе данных, заполняя информацию о товаре.

Вот как может выглядеть такой класс:

class Good{
  id: number;
  name: string;
  name_slug: string;
  desc: string;
  subcategory_id: number;
  subcategory: any;
  image: any;

  constructor(){}

  public static async serialize(json_obj: any){
    let obj = new Good();
    obj.id = json_obj.id;
    obj.name = json_obj.name;
    obj.name_slug = json_obj.name_slug;
    obj.desc = json_obj.desc;
    obj.subcategory_id = json_obj.subcategory_id;
    /// заполняем подкатегорию
    let sql_sub = 'SELECT * FROM shop_subcategory where id=$1';
    let result_sub = await pool.query(sql_sub,[obj.subcategory_id]);
    obj.subcategory = {'name': result_sub.rows[0].name, 'name_slug': result_sub.rows[0].name_slug};
    /// заполняем изображение
    let sql_image = 'SELECT * FROM shop_image where good_id=$1';
    let result_image = await pool.query(sql_image,[obj.id]);
    obj.image = {'image': result_image.rows[0].image};    
    return obj;
  }
 
}

Теперь при формировании ответа сервером, например при запросе детальной информации о товаре, нам достаточно вызвать статический метод и он создаст заполненный объект, который мы и вернем браузеру в виде json-а.

export class GoodAPI {

    async getOne(req: Request, res: Response){
       const sql = 'SELECT * FROM shop_good where id=$1';
       const result = await pool.query(sql,[req.params.id]);
       let good = await Good.serialize(result.rows[0]);
       res.json(good);
    }
}

Выводы

В данной статье рассмотрен процесс создания рабочего окружения для разработки серверной части приложения на базе NodeJS, описана типовая структура проекта, удовлетворяющая требованиям RESTfull API архитектуры. Освещены приемы работы с базой данных PostgreSQL и механизмы осуществления SQL запросов библиотекой node-postgres. Также была затронута тема unit тестирования с использованием библиотеки Jasmine.

0/5
Проголосовало людей: 0
СОДЕРЖАНИЕ
СТАТЬИ
Часть 1. Серверная
Инструментарий
Тестирование.
Установка.
Выводы
ПОЛУЧАТЬ ИНТЕРЕСНЫЕ СТАТЬИ
Уже подписались 244 человек
Автор
204
0
Дмитрий Жариков
Дмитрий
Жариков
most
Popular
Возможно
Из статьи вы узнаете 9 способов создать сильный бренд и 2 способа создать слабый. Но…
Надежда Тихонюк
Надежда Тихонюк
Мобильное приложение – просто MUST HAVE для современного предпринимателя и его бизнеса. Это дополнительная возможность…
Галина Назарова
Галина Назарова
Заработок на мобильных приложениях - не фантастика, а уже обычная практика. Главное знать, как это…
Елена Пименова
Елена Пименова
Давайте начнем
беседу!
КОММЕНТАРИИ0
ОСТАВИТЬ КОММЕНТАРИЙ К СТАТЬЕ
ПОДПИСЫВАЙТЕСЬ НА РАССЫЛКУ АЙТЫЖБЛОГ
ХОТИТЕ ПОЛУЧАТЬ 
ИНТЕРЕСНЫЕ СТАТЬИ?
Уже подписались 244 человек
313
ПОПИСЧИКОВ
ЧИТАТЬ
4295
ПОПИСЧИКОВ
СЛЕДИТЬ
9307
ПОПИСЧИКОВ
СЛЕДИТЬ