Node JS
Introdução a MVC - Model View Controller
A arquitetura MVC (Model-View-Controller) é um padrão de design de software que é amplamente utilizado para desenvolver aplicativos web, para usar basta separar os arquivos em diretórios com os nomes dos componentes que serão explicados mais abaixo. Ela é composta por três componentes principais:
Model
: O modelo representa a camada de dados do aplicativo. Ele lida com a manipulação de dados, como a interação com um banco de dados ou outros recursos externos. O modelo contém as regras de negócio e a lógica para manipular os dados.
View
: A visão é responsável por exibir a interface do usuário. Ela renderiza a interface do aplicativo e apresenta os dados fornecidos pelo modelo. A visão não contém lógica de negócios, mas é responsável por exibir os dados de maneira agradável para o usuário.
Controller
: O controlador é responsável por receber as solicitações do usuário, processá-las e coordenar a interação entre o modelo e a visão. Ele recebe os dados da solicitação, interage com o modelo para obter os dados necessários e, em seguida, determina qual visão deve ser renderizada para exibir a resposta ao usuário.
Estrutura com MVC:
Controllers
Diretório onde ficam os arquivos de controle.Models
Diretórios onde ficam os arquivos de Models, interação com banco de dados.Views
Diretórios onde ficam os arquivos de View (Handlebars).routes
Diretórios onde ficam os arquivos de rotas.index.js
Arquivo que inicializa a aplicação.
Criando o Model
Para criar um model
, coloque ele dentro do diretório models e no arquivo <model>.js
, exporte como um módulo.
const { DataTypes } = require('sequelize')
const db = require('../db/conn')
const Task = db.define('Task', {
title: {
type: DataTypes.STRING,
allowNull: false,
},
description: {
type: DataTypes.STRING,
},
done: {
type: DataTypes.BOOLEAN,
},
})
module.exports = Task
Agora importe o model dentro do arquivo base. Assim que rodar o código o banco será criado.
const express = require('express')
const exphbs = require('express-handlebars')
const app = express()
const conn = require('./db/conn')
const Task = require('./models/Task') // Importe o Model chamado 'Task' (tabela Task no DB)
app.engine('handlebars', exphbs.engine()) // define handlebars como template engine
app.set('view engine', 'handlebars')
app.use(
express.urlencoded({
extended: true,
}),
)
app.use(express.json())
app.use(express.static('public'))
conn
.sync()
.then(() => {
app.listen(3000)
})
.catch((err) => console.log(err))
A tabela é criada por causa do código abaixo:
const conn = require('./db/conn')
conn
.sync()
.then(() => {
app.listen(3000)
})
.catch((err) => console.log(err))
Criando o Controller
Os controllers devem ser colocados em um diretório separado, normalmente esse diretório chama controllers
. Ele será uma classe que contém as funções com a lógica de cada Rota. Precisamos importar os Models.
const Task = require('../models/Task')
module.exports = class TaskController {
static createTask(req, res) {
res.render('tasks/create')
}
static showTasks(req, res) {
res.render('tasks/all')
}
}
O código acima está criando um controlador chamado TaskController
que tem um método estático createTask
.
A linha const Task = require('../models/Task')
está importando o modelo Task
de um arquivo localizado em um diretório acima, dentro da pasta models
. Isso indica que o modelo Task
contém as informações necessárias para popular a tabela Task
no banco de dados.
O método createTask
é uma função estática que recebe dois argumentos: req
e res
. Dentro do método, a função res.render('tasks/create')
está sendo chamada. Isso vai ser utilizado em um contexto do Express.js para renderizar um template chamado create
. Isso sugere que a intenção é renderizar uma página onde o usuário pode criar uma nova tarefa.
Uma função normal, como function example() {...}
, é uma função que pertence a um objeto ou a um protótipo de objeto. Ela pode ser chamada usando a sintaxe object.example()
, onde object
é uma instância do objeto que possui a função. Funções normais podem acessar propriedades e métodos do objeto em que estão definidas.
Uma função estática, como static example() {...}
, é uma função que pertence a uma classe
(classe em JavaScript é uma função) e não a uma instância da classe. Ela pode ser chamada diretamente usando a sintaxe ClassName.example()
. Funções estáticas não têm acesso direto às propriedades e métodos da instância, pois são independentes dela.
Criando o Route
Os Routes devem ser colocados em um diretório separado, normalmente esse diretório chama routes
. Nele teremos que criar as rotas do sites, importar o Controller e para cada rota, associar uma função estática que foi criada no Controller.
const express = require('express')
const router = express.Router()
const TaskController = require('../controllers/TaskController')
// Criando as rotas e associando a funçao estática do nosso controller:
router.get('/add', TaskController.createTask)
router.get('/', TaskController.showTasks)
module.exports = router
Agora importe as rotas dentro do arquivo base. Defina também o middleware para ser usado com nossas rotas.
const express = require('express')
const exphbs = require('express-handlebars')
const app = express()
const conn = require('./db/conn')
// Models
const Task = require('./models/Task')
// routes
const taskRoutes = require('./routes/tasksRoutes')
app.engine('handlebars', exphbs.engine()) // define handlebars como template engine
app.set('view engine', 'handlebars')
app.use(
express.urlencoded({
extended: true,
}),
)
app.use(express.json())
app.use(express.static('public'))
app.use('/tasks', taskRoutes) // define um middleware que aplica o conjunto de rotas 'taskRoutes' a todas as rotas que começam com o caminho '/tasks'.
// Para mais detalhes veja: https://www.sysnetbr.eng.br/docs/Programacao/Nodejs/node2#m%C3%B3dulo-de-rotas
conn
.sync()
.then(() => {
app.listen(3000)
})
.catch((err) => console.log(err))
Salvando dados
Para salvar dados, vamos criar uma rota usando método post
.
const express = require('express')
const router = express.Router()
const TaskController = require('../controllers/TaskController')
router.get('/add', TaskController.createTask)
router.post('/add', TaskController.createTaskSave)
router.get('/', TaskController.showTasks)
module.exports = router
Agora vamos criar a função estática createTaskSave
na classe:
const Task = require('../models/Task')
module.exports = class TaskController {
static createTask(req, res) {
res.render('tasks/create')
}
static createTaskSave(req, res) {
const task = {
title: req.body.title,
description: req.body.description,
done: false, // Definindo done manualmente
}
// Chama 'Task' que é o model que cria e alimenta a tabela no banco de dados,
// e informa a var 'task' criada mais acima:
Task.create(task)
.then(res.redirect('/tasks'))
.catch((err) => console.log())
}
static showTasks(req, res) {
res.render('tasks/all')
}
}
Pegando itens do Banco
Para obter dados do banco, vamos criar uma rota usando método get
que acessa uma função do controller. Comece criando uma nova função no controller:
const Task = require('../models/Task')
module.exports = class TaskController {
static createTask(req, res) {
res.render('tasks/create')
}
static createTaskSave(req, res) {
const task = {
title: req.body.title,
description: req.body.description,
done: false,
}
Task.create(task)
.then(res.redirect('/tasks'))
.catch((err) => console.log())
}
static showTasks(req, res) {
Task.findAll({ raw: true }) // exibe os dados em array
.then((data) => {
res.render('tasks/all', { tasks: data }) // ALL é um handlebars
})
.catch((err) => console.log(err))
}
}
Crie o all.handlebars
:
<h1>Todas as Tasks:</h1>
<ul class="task-list">
{{#each tasks}}
<li>
<a href="/" class="title">{{this.title}}</a>
<span class="actions">
<a href="/"><i class="bi bi-check2"></i></a>
<a href="/"><i class="bi bi-pencil-square"></i></a>
<a href="/"><i class="bi bi-x-lg"></i></a>
</span>
</li>
{{/each}}
</ul>
bi bi-check2
define o ícone, para isso importe o ícone no arquivo de main.handlebars e adicione (dentro de head):<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
Isso chama bootstrap icons.
A rota já existe router.get('/', TaskController.showTasks)
, basta acessar a página inicial.
Removendo dados
Para remover dados do banco, vamos criar uma rota usando método destroy
que acessa uma função do controller. Vamos usar o método post
para enviar os dados de remoção. Comece criando uma nova rota:
const express = require('express')
const router = express.Router()
const TaskController = require('../controllers/TaskController')
router.get('/add', TaskController.createTask)
router.post('/add', TaskController.createTaskSave)
router.post('/remove', TaskController.removeTask)
router.get('/', TaskController.showTasks)
module.exports = router
Agora vamos criar a função no controller:
const Task = require('../models/Task')
module.exports = class TaskController {
static createTask(req, res) {
res.render('tasks/create')
}
static createTaskSave(req, res) {
const task = {
title: req.body.title,
description: req.body.description,
done: false,
}
Task.create(task)
.then(res.redirect('/tasks'))
.catch((err) => console.log())
}
static showTasks(req, res) {
Task.findAll({ raw: true })
.then((data) => {
let emptyTasks = false
if (data.length === 0) {
emptyTasks = true
}
res.render('tasks/all', { tasks: data, emptyTasks })
})
.catch((err) => console.log(err))
}
static removeTask(req, res) {
const id = req.body.id // vamos remover usando o ID da tabela
Task.destroy({ where: { id: id } })
.then(res.redirect('/tasks')) // Redireciona para /task ao final da operação
.catch((err) => console.log())
}
}
Ajuste o handlebars:
<h1>Todas as Tasks:</h1>
<ul class="task-list">
{{#if emptyTasks}}
<p>Não há tarefas cadastradas</p>
{{/if}}
{{#each tasks}}
<li>
<a href="/" class="title">{{this.title}}</a>
<span class="actions">
<a href="/"><i class="bi bi-check2"></i></a>
<a href="/"><i class="bi bi-pencil-square"></i></a>
<form action="/tasks/remove" method="POST">
<input type="hidden" name="id" value={{this.id}}>
<button type="submit">
<i class="bi bi-x-lg"></i>
</button>
</form>
</span>
</li>
{{/each}}
</ul>
Editando um item da tabela
Para editar dados do banco, precisamos pegar os dados e colocar no formulário em uma view
. Para isso vamos usar o Controller, que pegará os dados via Model
. Depois disso a gente cria uma rota que corresponda a um ID de uma Task, depois é só preencher os valores dos inputs. Comece criando uma nova rota:
const express = require('express')
const router = express.Router()
const TaskController = require('../controllers/TaskController')
router.get('/add', TaskController.createTask)
router.post('/add', TaskController.createTaskSave)
router.post('/remove', TaskController.removeTask)
router.get('/edit/:id', TaskController.updateTask)
router.get('/', TaskController.showTasks)
module.exports = router
Agora vamos criar a função no controller:
const Task = require('../models/Task')
module.exports = class TaskController {
static createTask(req, res) {
res.render('tasks/create')
}
static createTaskSave(req, res) {
const task = {
title: req.body.title,
description: req.body.description,
done: false,
}
Task.create(task)
.then(res.redirect('/tasks'))
.catch((err) => console.log())
}
static showTasks(req, res) {
Task.findAll({ raw: true })
.then((data) => {
let emptyTasks = false
if (data.length === 0) {
emptyTasks = true
}
res.render('tasks/all', { tasks: data, emptyTasks })
})
.catch((err) => console.log(err))
}
static removeTask(req, res) {
const id = req.body.id
Task.destroy({ where: { id: id } })
.then(res.redirect('/tasks'))
.catch((err) => console.log())
}
static updateTask(req, res) { // coleta os dados da tabela
const id = req.params.id
Task.findOne({ where: { id: id }, raw: true })
.then((data) => {
res.render('tasks/edit', { task: data })
})
.catch((err) => console.log())
}
}
Agora crie o handlebars chamado edit
:
<h1>Editando a task: {{task.title}}</h1>
<form action="/tasks/edit" method="POST">
<input type="hidden" name="id" value={{task.id}}>
<div class="form-control">
<label for="title">Título:</label>
<input type="text" name="title" placeholder="O que você vai fazer?" value="{{task.title}}">
</div>
<div class="form-control">
<label for="description">Descrição:</label>
<textarea type="text" name="description" placeholder="O que você vai fazer?">{{task.description}}</textarea>
</div>
<input type="submit" value="Editar">
</form>
Agora vamos criar uma nova função no Controller, para que possamos salvar os dados alterados na tabela.
const Task = require('../models/Task')
module.exports = class TaskController {
static createTask(req, res) {
res.render('tasks/create')
}
static createTaskSave(req, res) {
const task = {
title: req.body.title,
description: req.body.description,
done: false,
}
Task.create(task)
.then(res.redirect('/tasks'))
.catch((err) => console.log())
}
static showTasks(req, res) {
Task.findAll({ raw: true })
.then((data) => {
let emptyTasks = false
if (data.length === 0) {
emptyTasks = true
}
res.render('tasks/all', { tasks: data, emptyTasks })
})
.catch((err) => console.log(err))
}
static removeTask(req, res) {
const id = req.body.id
Task.destroy({ where: { id: id } })
.then(res.redirect('/tasks'))
.catch((err) => console.log())
}
static updateTask(req, res) {
const id = req.params.id
Task.findOne({ where: { id: id }, raw: true })
.then((data) => {
res.render('tasks/edit', { task: data })
})
.catch((err) => console.log())
}
static updateTaskPost(req, res) {
const id = req.body.id
const task = {
title: req.body.title,
description: req.body.description,
}
Task.update(task, { where: { id: id } })
.then(res.redirect('/tasks'))
.catch((err) => console.log())
}
}
A função updateTaskPost
é chamada quando o formulário de edição é submetido. No HTML do formulário de edição, o atributo action
do formulário está definido como "/tasks/edit"
e o método como POST
. Quando o formulário é submetido, ele envia uma requisição POST para a rota "/tasks/edit"
.
No arquivo routes/tasksRoutes.js
, existe essa rota para lidar com a requisição POST!
Essa rota está apontando para a função updateTaskPost
do TaskController
, que é responsável por receber os dados do formulário de edição e atualizar a entrada correspondente no banco de dados.