Lá vamos nós de novo falar de clean architecture... Mas agora não vamos mais falar de golang, e sim de Vue.js. Vamos implementar o fronted da nossa api da série de Clean Architecture com Golang em Vue.js.
Vamos lá! Nossa implementação do frontend deve ter os mesmos requisitos da nossa api:
- Uma listagem de produtos
- Um formulário para adicionar produtos na lista
Package by feature Pattern
Nesta estrutura de projeto, os pacotes contêm todas as classes necessárias para um recurso. A independência do pacote é assegurada colocando classes intimamente relacionadas no mesmo pacote. Aqui um post com um ótimo exemplo de como funciona.
Implementação
Primeira coisa que precisamos fazer é criar nosso projeto vue, ainda com o Vue 2 com typescript.
vue create clean-vue cd clean-vue vue add vuetify npm i axios npm i @types/axios --save-dev npm run serve
Projetinho rodando liso, bora codar!
Vamos apagar a pasta src/components
e estruturar o projeto da seguinte forma:
- src
- di
- module
- pagination
- domain
- model
- domain
- product
- const
- components
- repository
- domain
- model
- usecase
- controller
- view
- pagination
Agora tudo está dando erro, nada mais funciona! Calma lá, vamos estruturar nosso código que tudo se resolve. :D
Model
Primeira coisa é nós definirmos o model com o que volta da API no arquivo src/module/product/domain/model/product.ts
.
import { AxiosResponse } from "axios" interface ProductI { id?: number name?: string description?: string price?: number } class Product { id: number name: string description: string price: number constructor({ id = 0, name = "", description = "", price = 0.00 }: ProductI) { this.id = id this.name = name this.description = description this.price = price } } class ProductPagination { items: ProductI[] total: number constructor(response?: AxiosResponse) { this.items = response?.data?.items?.map((product: any) => new Product(product)) ?? [] this.total = response?.data?.total ?? 0 } } export { Product, ProductPagination }
E também o model de paginação default de toda a aplicação no arquivo src/module/pagination/domain/model/pagination.ts
.
interface PaginationI { page: number itemsPerPage: number sort: string descending: string search: string } class Pagination { page: number itemsPerPage: number sort: string descending: string search: string constructor({ page, itemsPerPage, sort, descending, search }: PaginationI) { this.page = page this.itemsPerPage = itemsPerPage this.descending = descending this.search = search this.sort = sort } } export { Pagination }
Repository
Com nossos models prontos, podemos já preparar nosso repository para manipular os endpoint dos nossos produtos.
Criaremos o arquivo src/module/product/repository/fetchProductsRepository.ts
.
import { Pagination } from '@/module/pagination/domain/model/pagination' import { ProductPagination } from '../domain/model/product' import { AxiosInstance } from 'axios' interface FetchProductsRepository { (pagination: Pagination): Promise<ProductPagination> } const fetchProductsRepository = (axios: AxiosInstance): FetchProductsRepository => async (pagination: Pagination) => { const response = await axios.get("/product", { params: pagination }) const productPagination = new ProductPagination(response) return productPagination } export { fetchProductsRepository, FetchProductsRepository }
E também criaremos o arquivo src/module/product/repository/createProductRepository.ts
.
import { Product } from '../domain/model/product' import { AxiosInstance } from 'axios' interface CreateProductRepository { (product: Product): Promise<Product> } const createProductRepository = (axios: AxiosInstance): CreateProductRepository => async (product: Product) => { const response = await axios.post("/product", product) return new Product(response?.data) } export { createProductRepository, CreateProductRepository }
Usecase
Com nossos repositories criados, podemos implementar nosso usecase de produtos.
Criaremos o arquivo src/module/product/domain/usecase/fetchProductsUseCase.ts
.
import { FetchProductsRepository } from "../../repository/fetchProductsRepository" import { Pagination } from "@/module/pagination/domain/model/pagination" import { ProductPagination } from "../model/product" import { DataOptions } from "vuetify" interface FetchProductsUseCase { (options: DataOptions, search: string): Promise<ProductPagination> } const fetchProductsUseCase = (repository: FetchProductsRepository): FetchProductsUseCase => async (options: DataOptions, search: string) => { const pagination = new Pagination({ descending: options.sortDesc.join(","), sort: options.sortBy.join(","), page: options.page, itemsPerPage: options.itemsPerPage, search: search, }) const productPagination = await repository(pagination) return productPagination } export { fetchProductsUseCase, FetchProductsUseCase }
E também criaremos o arquivo src/module/product/domain/usecase/createProductUseCase.ts
.
import { CreateProductRepository } from "../../repository/createProductRepository" import { Product } from "../model/product" interface CreateProductsUseCase { (product: Product): Promise<Product> } const createProductUseCase = (repository: CreateProductRepository): CreateProductsUseCase => async (product: Product) => { const productCreated = await repository(product) return productCreated } export { createProductUseCase, CreateProductsUseCase }
Controller
Com nossos usecases criados, podemos implementar nosso Controller no arquivo module/product/controller/productController.ts
.
import { CreateProductsUseCase } from "../domain/usecase/createProductUseCase"; import { FetchProductsUseCase } from "../domain/usecase/fetchProductUseCase"; import { Product, ProductPagination } from "../domain/model/product"; import { headers } from "../const/header"; class ProductController { options: any public product = new Product({}) public productPagination = new ProductPagination() public headers = headers public formDialog = false constructor( private context: any, private fetchProductsUseCase: FetchProductsUseCase, private createProductUseCase: CreateProductsUseCase ) { } async paginate() { this.productPagination = await this.fetchProductsUseCase(this.options, "") } async save() { if (this.context.$refs.productForm.$refs.form.validate()) { await this.createProductUseCase(this.product) this.cancel() this.paginate() } } cancel() { this.product = new Product({}) this.context.$refs.productForm.$refs.form.resetValidation() this.formDialog = false } } export { ProductController }
Tudo pronto! Brincadeira... Estamos quase lá, vamos configurar nossa injeção de dependências. Para configurar a injeção de dependência do nosso product vamos criar um arquivo em module/di/di.ts
.
import { fetchProductsRepository } from "../product/repository/fetchProductsRepository"; import { createProductRepository } from "../product/repository/createProductRepository"; import { createProductUseCase } from "../product/domain/usecase/createProductUseCase"; import { fetchProductsUseCase } from "../product/domain/usecase/fetchProductUseCase"; import { ProductController } from "../product/controller/productController"; import axios from "axios"; const axiosInstance = axios.create({ baseURL: "https://clean-go.herokuapp.com", headers: { "Content-Type": "application/json" } }) axiosInstance.interceptors.response.use((response) => response, async (err) => { const status = err.response ? err.response.status : null if (status === 500) { // Do something here or on any status code return } return Promise.reject(err); }); // Implementation methods from products feature const fetchProductsRepositoryImpl = fetchProductsRepository(axiosInstance) const fetchProductsUseCaseImpl = fetchProductsUseCase(fetchProductsRepositoryImpl) const createProductRepositoryImpl = createProductRepository(axiosInstance) const createProductUseCaseImpl = createProductUseCase(createProductRepositoryImpl) const productController = (context: any) => new ProductController( context, fetchProductsUseCaseImpl, createProductUseCaseImpl ) export { productController }
Agora sim, bora para nossa tela! Fique a vontade para montar ela do jeito que quiser!
Criaremos o arquivo module/product/components/productTable.vue
<template> <v-card> <v-card-title> Products <v-spacer></v-spacer> <v-btn color="primary" @click="controller.formDialog = true" > <v-icon left>mdi-plus</v-icon>new </v-btn> </v-card-title> <v-card-text class="pa-0"> <v-data-table dense :items="controller.productPagination.items" :headers="controller.headers" :options.sync="controller.options" @pagination="controller.paginate()" :server-items-length="controller.productPagination.total" ></v-data-table> </v-card-text> </v-card> </template> <script> export default { props: { controller: { require: true, }, }, }; </script>
E o arquivo module/product/components/productForm.vue
<template> <v-dialog persistent width="400" v-model="controller.formDialog" > <v-card> <v-card-title class="pa-0 pb-4"> <v-toolbar flat dense color="primary" class="white--text" > New product </v-toolbar> </v-card-title> <v-card-text> <v-form ref="form"> <v-text-field label="Name" dense filled v-model="controller.product.name" :rules="[(v) => !!v || 'Required']" ></v-text-field> <v-text-field label="Price" dense filled v-model.number="controller.product.price" :rules="[(v) => !!v || 'Required']" ></v-text-field> <v-textarea label="Description" dense filled v-model="controller.product.description" :rules="[(v) => !!v || 'Required']" ></v-textarea> </v-form> </v-card-text> <v-card-actions> <v-btn @click="controller.cancel()" color="red" text >cancel</v-btn> <v-spacer></v-spacer> <v-btn color="primary" @click="controller.save()" > <v-icon left>mdi-content-save</v-icon>save </v-btn> </v-card-actions> </v-card> </v-dialog> </template> <script> export default { props: { controller: { require: true, }, }, }; </script>
E por fim criaremos o arquivo module/product/view/product.vue
<template> <v-app> <v-main> <v-row class="fill-height" justify="center" align="center" > <v-col cols="12" lg="6" > <product-table ref="productTable" :controller="controller" /> <product-form ref="productForm" :controller="controller" /> </v-col> </v-row> </v-main> </v-app> </template> <script> import { productController } from "../../di/di"; import ProductTable from "../components/productTable"; import ProductForm from "../components/productForm"; export default { components: { ProductTable, ProductForm, }, data: (context) => ({ controller: productController(context), }), }; </script>
E a estrutura final ficou:
Top comments (0)