All Projects → suissa → framework-study-group

suissa / framework-study-group

Licence: other
Iremos criar NOSSO framework do 0 para entender seu funcionamento

framework-study-group

Iremos criar NOSSO framework do 0 para entender seu funcionamento parte a parte.

Rotas

Iremos nos basear no USO do Express, dessa forma:

const user = require('./modules/User/routes')
app.use('/api/user', user)
router.get('/', (req, res) => {
  res.json([1,2,3,4])
})

HTTP - createServer

Para criarmos um servidor simples de rotas com o módulo http podemos fazer assim:

const http = require('http');
const querystring = require('querystring');


const teste = {
    Name: 'Adelmo Junior',
    Age: 17
}

const server = http.createServer(function(req, res){
    res.writeHead(200, {"Content-Type": "application/json"});

    var url = req.url;
    var method = req.method;

    if(method === 'GET' && url === '/' ){
        res.writeHead(200, {"Content-Type": "application/json"});
        res.write(JSON.stringify(teste));
        res.end()
    }else{
        res.writeHead(404);
        res.write('<h1>ERROR</h1>');
    }

    if(method === 'POST' && url === '/post'){

        var data = '';

        req.on('data', function(body){

            data += body;

        });
        req.on('end', function(){
            res.writeHead(200, {"Content-Type": "application/json"});

            console.log(data)
            var value = {
                "sucess": true
            }
            res.end('OK');
        });
    }else{
        res.writeHead(404);
        res.end('<html><body>404</body></html>');
    }
});
server.listen(3000, function(){
    console.log('Server rodando de boas :D');
});

Conceitos

Precisamos enteder como é o fluxo de uma rota nesse framework:

  1. Chega uma requisição;
  2. Testa qual método da requisição;
  3. Testa qual a url da requisição;
  4. Executa a ação definida pela rota.

Porém você precisa se questionar:

Como eu defino essas rotas para serem testadas?

Ótima pergunta!

Rotas - Definição

Sabemos que precisamos criar uma definição de rotas igual ao do Express:

router.get('/', (req, res) => {
  res.json([1,2,3,4])
})

Logo precisamos criar um Objeto router que possui a função get, a qual recebe 2 parâmetros:

  • path: url da rota;
  • action: a função que precisa ser executada nessa rota.

Agora sabendo disso podemos criar o seguinte módulo:

// lib/router.js
const router = {
  routes: [],
  get: (path, action) => {
    router.routes.push({
      method: 'GET',
      path,
      action
    })
  }
}

module.exports = router

Dessa forma nós já criamos um mecanismo de definição de rotas IGUAL ao do Express.

Olhe como iremos utilizar no nosso arquivo routes.js:

// routes.js
const router = require('./lib/router')
const Controller = require('./controller')

const teste = [{
  Name: 'Adelmo Junior',
  Age: 17
}]

router.get('/', (req, res) => {
  Controller.find(req, res)(teste)
})
router.get('/123', (req, res) => {
  Controller.findOne(req, res)(teste)
})
console.log('router: ', router)
/**
router:  { routes: 
   [ { method: 'GET', path: '/', action: [Function] },
     { method: 'GET', path: '/123', action: [Function] } ],
  get: [Function: get] }
*/
module.exports = route

Logo mais chegarei nessa parte do Controller, por hora vamos nos focar nas rotas e para isso agora precisamos fazer o mecanismo que executa essas rotas.

Percebeu que essa definição das rotas apenas adiciona seus dados no Array router.routes, fiz dessa forma para facilitar o mecanismo de execução da rota desejada, então vamos entender como fazer isso.

Rotas - Execução da Requisição

Primeiramente você precisa entender em que momento uma requisição nova chega no server, para isso vamos montar o seguinte index.js com a criação do server HTTP e vamos usar o evento request assim:

// index.js
const http = require('http')
const server = http.createServer()

const app = require('./lib')
const routes = require('./routes')

// Essa é a forma explícita
// server.on('request', (req, res) => app.use(req, res)(routes))

// Essa é a forma implícita
server.on('request', app.use(routes))

server.listen( 3000, () => {
  console.log( 'Server rodando de boas :D' )
} )

Como sabemos que a chamada é app.use(req, res)(routes) logo inferimos que precisamos criar um módulo que seja um Objeto com a chave use, onde a qual é uma CLOSURE pois é uma função que retorna outra, facilmente notada pela sua execução: (req, res)(routes).

Basicamente esse esqueleto:

const teste = [
  {
    id: 0,
    name: 'Adelmo Junior',
    age: 17
  },{
    id: 1,
    name: 'Suisseba da Periferia',
    age: 33
  }
]

module.exports = {
  use: (router) => (req, res) => {

    const url = req.url
    const method = req.method.toLowerCase()

    switch (method) {
      case 'get': {
        switch (url) {
          case '/': {
            res.writeHead(200, { 'Content-Type': 'application/json' })
            res.write(JSON.stringify(teste))
            res.end()
            break;
          }
          case '/get': {
            res.writeHead(200, { 'Content-Type': 'application/json' })
            res.write(JSON.stringify(teste[0]))
            res.end()
            break;
          } 
          default: {
            res.writeHead(404, { 'Content-Type': 'application/json' })
            res.write(JSON.stringify({status: 'error', message: 'Rota não encontrada'}))
            res.end()
            break;
          }
        }
      break;
      }
      default: {
        res.writeHead(404, { 'Content-Type': 'application/json' })
        res.write(JSON.stringify({status: 'error', message: 'Rota não encontrada'}))
        res.end()
        break;
      }
    }
  }
}

Já entendemos como iremos testar e executar nossas rotas.

No primeiro switch nós testamos qual o método HTTP da requisição para cair no seu case correto para depois testar sua rota, porém como já possuímos o Objeto router que possui internamente um Array com as nossas rotas, logo nós devemos criar um mecanismo para essa validação e execução de uma forma mais genérica sem esse segundo switch que seria para cada rota.

Para solucionar esse problema iremos criar uma função genérica que será executada em qualquer requisição GET e com isso irá buscar o Objeto da rota correta e executar sua ação, essa foi minha solução simplista:

const byPath = (url) => ({ path }) => path === url

const getRoutes = (router, method = 'get') => 
  router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())

switch (method) {
  case 'get': {
    getRoutes(router, 'get')
      .find(byPath(url))
      .action(req, res)
    break;
  }
  default:
    break;
}

Agora olhe como é o retorno de cada uma dessas funções:

const byPath = (url) => ({ path }) => path === url
/**
{ method: 'GET', path: '/', action: [Function] }
*/

const getRoutes = (router, method = 'get') => 
  router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())
/**
[ 
  { method: 'GET', path: '/', action: [Function] },
  { method: 'GET', path: '/123', action: [Function] } 
]
*/

switch (method) {
  case 'get': {
    getRoutes(router, 'get')
      .find(byPath(url))
      .action(req, res)
    break;
  }
  default:
    break;
}

Com a getRoutes nós recebemos um Array com os Objetos da rotas que são do mesmo método, logo depois utilizo a função find, entretando note como eu defini a função byPath:

const byPath = (url) => ({ path }) => path === url

Porém a forma mais comum de se escrever isso é assim:

const byPath = (url) => (route) => route.path === url

Bom como DEFINIMOS que esse é nosso padrão de configuração da rota, nós TEMOS CERTEZA que o Objeto que está contido no Array routes possui a seguinte estrutura imutável:

  • method;
  • path;
  • action.

Eu usei ({ path }) => path === url pois queria pegar apenas o valor dessa propriedade, isso foi possível graças à Atribuição via desestruturação (destructuring assignment), já aproveitando o ensejo vamos criar uma validação para a requisição do favicon.ico para retornarmos false.

const byPath = (url) => ({ path }) => path === url

const getRoutes = (router, method = 'get') => 
  router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())

module.exports = {
  use: (router) => async (req, res) => {
    const url = req.url
                            
    const method = req.method.toLowerCase()
    
    if (url.includes('favicon.ico'))
      return false
    
    switch (method) {
      case 'get': {
        getRoutes(router, 'get')
          .find(byPath(url))
          .action(req, res)
        break;
      }
      default:
        break;
    }
  }
}

Além disso vamos trocar getRoutes(router, 'get') por getRoutes(router, method) para que dessa forma possamos extender esse código facilmente e de maneira genérica, por exemplo:

const byPath = (url) => ({ path }) => path === url

const getRoutes = (router, method = 'get') => 
  router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())

module.exports = {
  use: (router) => async (req, res) => {
    const url = req.url
                            
    const method = req.method.toLowerCase()
    
    if (url.includes('favicon.ico'))
      return false
    
    switch (method) {
      case 'get': {
        getRoutes(router, method)
          .find(byPath(url))
          .action(req, res)
        break;
      }
      case 'post': {
        getRoutes(router, method)
          .find(byPath(url))
          .action(req, res)
        break;
      }
      default:
        break;
    }
  }
}

Percebeu que para criarmos para os métodos PUT e DELETE precisamos fazer apenas isso:

const byPath = (url) => ({ path }) => path === url

const getRoutes = (router, method = 'get') => 
  router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())

module.exports = {
  use: (router) => async (req, res) => {
    const url = req.url
                            
    const method = req.method.toLowerCase()
    
    if (url.includes('favicon.ico'))
      return false
    
    switch (method) {
      case 'get': {
        getRoutes(router, method)
          .find(byPath(url))
          .action(req, res)
        break;
      }
      case 'post': {
        getRoutes(router, method)
          .find(byPath(url))
          .action(req, res)
        break;
      }
      case 'put': {
        getRoutes(router, method)
          .find(byPath(url))
          .action(req, res)
        break;
      }
      case 'delete': {
        getRoutes(router, method)
          .find(byPath(url))
          .action(req, res)
        break;
      }
      default:
        break;
    }
  }
}

ATENÇÃO!!!!


O que você notou nesse código acima???

Veja novamente apenas a parte que interessa:

switch (method) {
  case 'get': {
    getRoutes(router, method)
      .find(byPath(url))
      .action(req, res)
    break;
  }
  case 'post': {
    getRoutes(router, method)
      .find(byPath(url))
      .action(req, res)
    break;
  }
  case 'put': {
    getRoutes(router, method)
      .find(byPath(url))
      .action(req, res)
    break;
  }
  case 'delete': {
    getRoutes(router, method)
      .find(byPath(url))
      .action(req, res)
    break;
  }
  default:
    break;
}



SIM!!! Ele é quase TODO IGUAL!!!




Retirando os códigos duplicados ficamos com isso:

const byPath = (url) => ({ path }) => path === url

const getRoutes = (router, method = 'get') => 
  router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())

module.exports = {
  use: (router) => async (req, res) => {
    const url = req.url
                            
    const method = req.method.toLowerCase()
    
    if (url.includes('favicon.ico'))
      return false
    
    return getRoutes(router, method)
      .find(byPath(url))
      .action(req, res)
  }
}

Refatoração NERVOSA - Mas simples

Acompanhe comigo o seguinte, vamos retirar a definição das constantes internas e usar seu valor diretamente como visto abaixo:

const byPath = (url) => ({ path }) => path === url

const getRoutes = (router, method = 'get') => 
  router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())

module.exports = {
  use: (router) => async (req, res) => {
    
    if (req.url.includes('favicon.ico'))
      return false
    
    return getRoutes(router, req.method.toLowerCase())
      .find(byPath(req.url))
      .action(req, res)
  }
}

Perceba que nossa função pode ser separada em 2 partes:

if (req.url.includes('favicon.ico'))
  return false
return getRoutes(router, req.method.toLowerCase())
  .find(byPath(req.url))
  .action(req, res)

Sabendo disso nós podemos FACILMENTE refatorar para um IF ternário, para deixarmos nossa função com APENAS UMA FUCKING LINHA:

const byPath = (url) => ({ path }) => path === url

const getRoutes = (router, method = 'get') => 
  router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())

module.exports = {
  use: (router) => async (req, res) => 
    (req.url.includes('favicon.ico'))
      ? false
      : getRoutes(router, req.method.toLowerCase())
        .find(byPath(req.url))
        .action(req, res)
  }
}

Router

Com isso podemos definir as outras funções do nosso router:

// lib/router.js
const router = {
  routes: [],
  get: (path, action) => {
    router.routes.push({
      method: 'GET',
      path,
      action
    })
  },
  post: (path, action) => {
    router.routes.push({
      method: 'POST',
      path,
      action
    })
  },
  put: (path, action) => {
    router.routes.push({
      method: 'PUT',
      path,
      action
    })
  },
  delete: (path, action) => {
    router.routes.push({
      method: 'DELETE',
      path,
      action
    })
  }
}

module.exports = router

OBVIAMENTE VOCÊ PERCEBEU QUE O CÓDIGO ESTÁ SE REPETINDO. Logo nós DEVEMOS refatorar ele para encapsular sua lógica em uma função genérica para reusarmos, dessa forma:

const addRoute = (router, method, path, action) => {
  router.routes.push({
    method,
    path,
    action
  })
}

const router = {
  routes: [],
  get: (path, action) => addRoute(router, 'GET', path, action),
  post: (path, action) => addRoute(router, 'POST', path, action),
  put: (path, action) => addRoute(router, 'PUT', path, action),
  delete: (path, action) => addRoute(router, 'DELETE', path, action)
}

module.exports = router

Dessa forma saimos de 33 linhas para 17!

Quase a metade! Tá bom né?



Request

Request - params

No Express quando definimos uma rota que aceita parâmetros precisamos disponibilizar esses parâmetros e seus valores, que vieram no req.url, como um Objeto dentro de req.params.

router.get('/', (req, res) => {
})

router.get('/:id', (req, res) => {
})

router.get('/:id/:name', (req, res) => {
})

Primeiro precisamos pensar em como armazenar essas rotas pois eu não posso salva-las com esas urls, tendo em vista que nosso router iria procurar EXATAMENTE por essa rota e obviamente a url enviada estará com os valores que devem ser colocados nessas variáveis.

Imagine que teremos 3 requisições:

GET /
GET /1
GET /1/suissa

Como que iremos tratar essas urls para que possamos buscar a rota correta?

A solução que pensei foi o seguinte:

Analisando as rotas podemos perceber que elas possuem quantidade de parâmetros diferentes, porém todas iniciam na /, sabendo disso nós podemos armazenar a url de cada rota apenas com /, todavia precisamos criar uma lógica para separar os parâmetros da url e depois colocar corretamente em req.params no momento que a requisição chegar no nosso servidor.

Nesse momento precisamos inferir qual a lógica para a separação desses parâmetros, então observe abaixo:

> "/".split("/")
[ '', '' ]
> "/:id".split("/")
[ '', ':id' ]
> "/:id/:name".split("/")
[ '', ':id', ':name' ]

Depois dessa quebra da url vamos eliminar os valores vazios:

> "/".split("/").filter(e => e !== '')
[]
> "/:id".split("/").filter(e => e !== '')
[ ':id' ]
> "/:id/:name".split("/").filter(e => e !== '')
[ ':id', ':name' ]

Já podemos colocar essa lógica em uma função:

const getValuesFromURL = (path) => path.split('/').filter(e => e !== '')

Como queremos apenas o nome dos parâmetros precisamos eliminar o : com um map:

> "/:id".split("/").filter(e => e !== '').map(p => p.replace(':', ''))
[ 'id' ]
> "/:id/:name".split("/").filter(e => e !== '').map(p => p.replace(':', ''))
[ 'id', 'name' ]

Passamos essa lógica para uma função a qual irá receber o RESULTADO da getValuesFromURL, por exemplo:

const hasParams = getParams(getValuesFromURL(path))

Então criamos a função getParams que irá OU receber um Array com os parâmetros OU false:

const getParams = (arrParams) => 
  (arrParams.length) 
    ? arrParams.map(p => p.replace(':', '')) 
    : false
const addRoute = (router, method, path, action) => {
  const hasParams = getParams(getValuesFromURL(path))
  path = (hasParams) ? '/' : path
  
  router.routes.push({
    method,
    path,
    action,
    hasParams
  })
}

Note essa definição path = (hasParams) ? '/' : path que foi feita para definir a url da rota com /, pois caso possua parâmetros sua url será /, senão será o próprio path definido na rota.

Juntando tudo agora temos:

// lib/router.js
const getParams = (arrParams) => 
  (arrParams.length) 
    ? arrParams.map(p => p.replace(':', '')) 
    : false
    
const getValuesFromURL = (path) => path.split('/').filter(e => e !== '')

const addRoute = (router, method, path, action) => {
  const hasParams = getParams(getValuesFromURL(path))
  path = (hasParams) ? '/' : path
  
  router.routes.push({
    method,
    path,
    action,
    hasParams
  })
}
const router = {
  routes: [],
  get: (path, action) => addRoute(router, 'GET', path, action),
  post: (path, action) => addRoute(router, 'POST', path, action),
  put: (path, action) => addRoute(router, 'PUT', path, action),
  delete: (path, action) => addRoute(router, 'DELETE', path, action)
}

module.exports = router

Para entendermos melhor como ficaram nossas rotas observe o log abaixo:

router.get('/', (req, res) => {})
router.get('/:id', (req, res) => {})
router.get('/:id/:name', (req, res) => {})

/**
[ { method: 'GET', path: '/', action: [Function], hasParams: false },
  { method: 'GET',
    path: '/',
    action: [Function],
    hasParams: [ 'id' ] },
  { method: 'GET',
    path: '/',
    action: [Function],
    hasParams: [ 'id', 'name' ] 
  } 
]
*/

Depois disso precisamos criar o mecanismo que valida esses parâmetros q adiciona esse objeto no req, para isso iremos modificar nossa função do use para que depois que acharmos a rota nós criemos o req.params para que quando executarmos route.action(req, res) esse req esteja correto.

E quase sempre que quisermos criar um Objeto de forma dinâmica e usando dados de 2 lugares diferentes nós usaremos o reduce, além disso a nossa função do reduce precisa ser uma closure, pois como iremos iterar em um Array, que possui os valores dos parâmetros que vieram na url da requisição, precisamos também injetar o Objeto da rota para que possamos pegar também o nome definido para cada parâmetro:

const params = getValuesFromURL(req.url)

const toParams = (route) => (obj, cur, i) => 
  Object.assign( obj, { [route.hasParams[i]]: cur } )

req.params = params.reduce(toParams(route), {})
// { id: '1', name: 'suissa' }

Substituindo nosso antigo use por esse novo:

const getValuesFromURL = (path) => path.split('/').filter(e => e !== '')
const byPath = (url) => ({ path }) => path === url
const getRoutes = (router, method = 'get') => 
  router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())

module.exports = {
  use: (router) => async (req, res) => {

    if (req.url.includes('favicon.ico'))
      return false

    const route = getRoutes(router, req.method.toLowerCase())
                    .find(byPath(req.url))

    req.params = getValuesFromURL(req.url).reduce(toParams(route), {})
    return  route.action(req, res)
  }
}
const getValuesFromURL = (path) => path.split('/').filter(e => e !== '')

const byPath = (url) => (route) => {
  const path = route.path
  const arrParams = getValuesFromURL(url)
  const hasParams = (arrParams.length) ? arrParams : false

  if (!route.hasParams) return (path === url) 
  
  return (hasParams) 
    ? (path === '/' && route.hasParams.length === hasParams.length)
    : (path === url) 
}

const getRoutes = (router, method = 'get') => {
  return router
    .routes
    .filter(route => route.method.toLowerCase() === method.toLowerCase())
}

const toParams = (route) => (obj, cur, i) => {
  return Object.assign( obj, { [route.hasParams[i]]: cur } )
}
module.exports = {
  use: (router) => async (req, res) => {
    if (req.url.includes('favicon.ico'))
      return false

    const route = getRoutes(router, req.method.toLowerCase())
        .find(byPath(req.url))

    const params = getValuesFromURL(req.url)

    req.params = (!route.hasParams) 
      ? { }
      : params.reduce(toParams(route), {})

    return  route.action(req, res)
  }
}

Request - query

Request - body

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].