Back-End

27 ago, 2018

Trabalhando com o SAPUI5 localmente e implementando no SCP

Publicidade

Quando trabalho com projetos SAPUI5, normalmente uso o WebIDE. O WebIDE é uma ótima ferramenta, mas estou mais confortável trabalhando localmente com meu IDE local.

Eu tenho essa ideia em mente, mas nunca encontro tempo pra trabalhar nela. Finalmente, depois de encontrar esse projeto de Holger Schäfer no GitHub, percebi como é fácil e comecei a trabalhar com esse projeto e adaptá-lo às minhas necessidades.

A base desse projeto é localneo. Localneo inicia um servidor http com base no arquivo neo-app.json. Isso significa que usaremos a mesma configuração que nós tínhamos na produção (no SCP). Claro que vamos precisar de destinos.

Precisamos apenas de um arquivo extra chamado destination.json, onde configuraremos nossos destinos (ele cria um proxy http, nada mais).

Neste projeto, criarei um aplicativo de exemplo simples que funciona com um servidor de API.

O back-end

Usarei neste exemplo um aplicativo PHP/Lumen:

$app->router->group(['prefix' => '/api', 'middleware' => Middleware\AuthMiddleware::NAME], function (Router $route) {
    $route->get('/', Handlers\HomeHandler::class);
    $route->post('/', Handlers\HomeHandler::class);
});

Basicamente tem duas rotas. De fato, ambas as rotas são iguais. Uma aceita a solicitação POST e outra solicitações GET.

Elas responderão com a data atual em um arquivo json.

namespace App\Http\Handlers;
 
class HomeHandler
{
    public function __invoke()
    {
        return ['date' => (new \DateTime())->format('c')];
    }
}

Ambas as rotas estão sob um middleware para fornecer a autenticação.

namespace App\Http\Middleware;
 
use Closure;
use Illuminate\Http\Request;
 
class AuthMiddleware
{
    public const NAME = 'auth';
 
    public function handle(Request $request, Closure $next)
    {
        $user = $request->getUser();
        $pass = $request->getPassword();
 
        if (!$this->validateDestinationCredentials($user, $pass)) {
            $headers = ['WWW-Authenticate' => 'Basic'];
 
            return response('Backend Login', 401, $headers);
        }
 
        $authorizationHeader = $request->header('Authorization2');
        if (!$this->validateApplicationToken($authorizationHeader)) {
            return response('Invalid token ', 403);
        }
 
        return $next($request);
 
    }
 
    private function validateApplicationToken($authorizationHeader)
    {
        $token = str_replace('Bearer ', null, $authorizationHeader);
 
        return $token === getenv('APP_TOKEN');
    }
 
    private function validateDestinationCredentials($user, $pass)
    {
        if (!($user === getenv('DESTINATION_USER') && $pass === getenv('DESTINATION_PASS'))) {
            return false;
        }
 
        return true;
    }
}

Isso significa que nosso serviço precisará de Autenticação Básica e também de autenticação baseada em Token.

O front-end

Nosso aplicativo ui5 usará um destino chamado BACKEND. Nós vamos configurá-lo em nosso arquivo neo-app.json.

...
{
  "path": "/backend",
  "target": {
    "type": "destination",
    "name": "BACKEND"
  },
  "description": "BACKEND"
}
...

Agora criaremos nosso arquivo extra chamado destinations.json. Localneo usará esse arquivo para criar um servidor da Web para servir nosso front-end localmente (usando o destino).

Como eu disse antes, nosso back-end precisará de uma autenticação básica. Essa autenticação será configurada na configuração de destino.

{
  "server": {
    "port": "8080",
    "path": "/webapp/index.html",
    "open": true
  },
  "service": {
    "sapui5": {
      "useSAPUI5": true,
      "version": "1.54.8"
    }
  },
  "destinations": {
    "BACKEND": {
      "url": "http://localhost:8888",
      "auth": "superSecretUser:superSecretPassword"
    }
  }
}

Nosso aplicativo será uma lista simples de itens.

<mvc:View controllerName="gonzalo123.controller.App" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m">
  <App id="idAppControl">
    <pages>
      <Page title="{i18n>appTitle}">
        <content>
          <List>
            <items>
              <ObjectListItem id="GET" title="{i18n>get}"
                              type="Active"
                              press="getPressHandle">
                <attributes>
                  <ObjectAttribute id="getCount" text="{/Data/get/count}"/>
                </attributes>
              </ObjectListItem>
              <ObjectListItem id="POST" title="{i18n>post}"
                              type="Active"
                              press="postPressHandle">
                <attributes>
                  <ObjectAttribute id="postCount" text="{/Data/post/count}"/>
                </attributes>
              </ObjectListItem>
            </items>
          </List>
        </content>
      </Page>
    </pages>
  </App>
</mvc:View>

Quando clicamos em GET, realizamos uma solicitação GET para o back-end e incrementamos o contador. O mesmo acontece com o POST.

Também mostraremos a data fornecida pelo back-end em um MessageToast.

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/model/json/JSONModel",
  'sap/m/MessageToast',
  "gonzalo123/model/api"
], function (Controller, JSONModel, MessageToast, api) {
  "use strict";
 
  return Controller.extend("gonzalo123.controller.App", {
    model: new JSONModel({
      Data: {get: {count: 0}, post: {count: 0}}
    }),
 
    onInit: function () {
      this.getView().setModel(this.model);
    },
 
    getPressHandle: function () {
      api.get("/", {}).then(function (data) {
        var count = this.model.getProperty('/Data/get/count');
        MessageToast.show("Pressed : " + data.date);
        this.model.setProperty('/Data/get/count', ++count);
      }.bind(this));
    },
 
    postPressHandle: function () {
      var count = this.model.getProperty('/Data/post/count');
      api.post("/", {}).then(function (data) {
        MessageToast.show("Pressed : " + data.date);
        this.model.setProperty('/Data/post/count', ++count);
      }.bind(this));
    }
  });
});

Começando nosso aplicativo localmente

Agora só precisamos iniciar o back-end:

php -S 0.0.0.0:8888 -t www

E o frontend:

localneo

Depurando localmente

Como estamos trabalhando localmente, podemos usar o depurador local no back-end e podemos usar pontos de interrupção, inspecionar variáveis, etc.

Também podemos depurar o front-end usando as ferramentas de desenvolvedor do Chrome. Também podemos mapear nosso sistema de arquivos locais no navegador e podemos salvar arquivos diretamente no Chrome.

Testando

Podemos testar o backend usando phpunit e executar nossos testes com:

teste de execução do compositor

Aqui podemos ver um teste simples do backend:

public function testAuthorizedRequest()
{
    $headers = [
        'Authorization2' => 'Bearer superSecretToken',
        'Content-Type'   => 'application/json',
        'Authorization'  => 'Basic ' . base64_encode('superSecretUser:superSecretPassword'),
    ];
 
    $this->json('GET', '/api', [], $headers)
        ->assertResponseStatus(200);
    $this->json('POST', '/api', [], $headers)
        ->assertResponseStatus(200);
}
 
 
public function testRequests()
{
 
    $headers = [
        'Authorization2' => 'Bearer superSecretToken',
        'Content-Type'   => 'application/json',
        'Authorization'  => 'Basic ' . base64_encode('superSecretUser:superSecretPassword'),
    ];
 
    $this->json('GET', '/api', [], $headers)
        ->seeJsonStructure(['date']);
    $this->json('POST', '/api', [], $headers)
        ->seeJsonStructure(['date']);
}

Como o Backend já foi testado, vamos simular o back-end aqui usando o servidor sinon (https://sinonjs.org/).

...
    opaTest("When I click on GET the GET counter should increment by one", function (Given, When, Then) {
      Given.iStartMyApp("./integration/Test1/index.html");
      When.iClickOnGET();
      Then.getCounterShouldBeIncrementedByOne().and.iTeardownMyAppFrame();
    });
 
    opaTest("When I click on POST the POST counter should increment by one", function (Given, When, Then) {
      Given.iStartMyApp("./integration/Test1/index.html");
      When.iClickOnPOST();
      Then.postCounterShouldBeIncrementedByOne().and.iTeardownMyAppFrame();
    });
...

A configuração do nosso servidor sinon:

sap.ui.define(
  ["test/server"],
  function (server) {
    "use strict";
 
    return {
      init: function () {
        var oServer = server.initServer("/backend/api");
 
        oServer.respondWith("GET", /backend\/api/, [200, {
          "Content-Type": "application/json"
        }, JSON.stringify({
          "date": "2018-07-29T18:44:57+02:00"
        })]);
 
        oServer.respondWith("POST", /backend\/api/, [200, {
          "Content-Type": "application/json"
        }, JSON.stringify({
          "date": "2018-07-29T18:44:57+02:00"
        })]);
      }
    };
  }
);

O processo de construção

Antes de fazer o upload do aplicativo para o SCP, precisamos criá-lo. O processo de criação otimiza os arquivos e cria o arquivo Component-preload.js e sap-ui-cachebuster-info.json (para garantir que nossos usuários não usem uma versão em cache de nosso aplicativo).

Vamos usar grunt/grunhido para construir nosso aplicativo. Aqui podemos ver nosso Gruntfile.js.

module.exports = function (grunt) {
  "use strict";
 
  require('load-grunt-tasks')(grunt);
  require('time-grunt')(grunt);
 
  grunt.config.merge({
    pkg: grunt.file.readJSON('package.json'),
    watch: {
      js: {
        files: ['Gruntfile.js', 'webapp/**/*.js', 'webapp/**/*.properties'],
        tasks: ['jshint'],
        options: {
          livereload: true
        }
      },
 
      livereload: {
        options: {
          livereload: true
        },
        files: [
          'webapp/**/*.html',
          'webapp/**/*.js',
          'webapp/**/*.css'
        ]
      }
    }
  });
 
  grunt.registerTask("default", [
    "clean",
    "lint",
    "build"
  ]);
};

Em nosso Gruntfile, também configurei um observador para criar o aplicativo automaticamente e acionar o recarregamento ao vivo (para recarregar meu navegador toda vez que eu alterar o front-end).

Agora eu posso construir a pasta dist com o comando:

grunt

Implantar no SCP

O processo de implantação está muito bem explicado no repositório do Holger. Basicamente, precisamos baixar o construtor MTA Archive e extraí-lo para ./ci/tools/mta.jar.

Também precisamos do SDK do Ambiente SAP Cloud Platform Neo (./ci/tools/neo-java-web-sdk/). Nós podemos baixar esses binários daqui.

Então precisamos preencher nossas credenciais scp em ./ci/deploy-mta.properties e configurar nosso aplicativo em ./ci/mta.yaml.

Por fim, executaremos ./ci/deploy-mta.sh (aqui podemos configurar nossa senha scp para inseri-la em cada implementação).

Código completo (front-end e back-end) na minha conta do GitHub.

***

Gonzalo Ayuso faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://gonzalo123.com/2018/08/20/working-with-sapui5-locally-and-deploying-in-scp/