click fraud detection
0 800 755 007
(Безкоштовно по Україні)

IM на основі NodeJS Express та Angular 7

7143

Частина 1. Серверна

Почнемо з моделі даних нашої програми, яка може бути представлена наступною схемою:

частина 1. серверна

Де ми маємо 4 таблиці з категоріями та підкатегоріями, товарами та зображеннями. Усі таблиці пов'язані одна з одною зовнішніми ключами.

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

Будемо використовувати СУБД PoptgresSQL.

Почнемо із серверної частини.

RESTfull API сервер на NodeJS і Express

Інструментарій

Насамперед налаштуємо наше робоче оточення й зробимо максимально зручним процес розробки та налаштування.

Для встановлення NodeJS та приведення його в актуальний стан в OC на основі Debian виконаємо наступну послідовність команд:

 sudo apt install nodejs
sudo npm cache clean -f
sudo npm install -gn
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 з параметрами транспілятора 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.

запустити налагодження, натиснувши на посилання inspect

Робота з базою даних PostgreSQL.

Тепер, коли ми налаштували робоче оточення, можна розпочати розробку REST API сервісу. На початку з'єднаємося з базою даних та отримаємо записи з таблиці категорій нашого магазину штор.

робота з базою даних postgresql

Для роботи з базою даних ми використовуватимемо пакет 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

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

результати - expected 404 to be 200

Приклад перевірки запиту на створення категорії та передачі даних у форматі 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.

p align="justify"> Особливу увагу слід приділити формуванню json відповіді, що описує товар. Проблема полягає в тому, що формат опису товару може згодом змінюватись. До нього можуть додаватися нові поля й масиви, рейтинги, коментарі та інше. Якщо ми створимо клас товару, то конструктор у разі не підійде, т.к. в ньому ми використовуватимемо асинхронні операції await/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.

У вас залишились запитання?

Залиште контактні дані. Наш менеджер зв'яжеться та проконсультує вас.

5/5
Корисність
Проголосували 6
Як вам стаття?
Давайте обговоримо Ваш проект
Давайте почнемо розмову!
КОМЕНТАРІ0
Можливо
CRM система - незамінний помічник сучасного бізнесу, про можливості та переваги якого написана не одна…
Василій Іздебський
Василій Іздебський
Target (таргет) у перекладі з англійської означає «мету». Таргетинг - це механізм маркетингу, що дозволяє…
Wezom
Wezom
Мобільний додаток для складу виконує низку важливих завдань, серед яких: організація та облік товарів на…
Wezom
Wezom
ПІДПИСУЙТЕСЯ НА РОЗСИЛКУ АЙТІБЛОГ
Бажаєте отримувати 
цікаві статті?