Nuxt aplikácia v kontajneroch – Riešime Vue.js #1


Nuxt aplikácia v kontajneroch – Riešime Vue.js #1

Frontend development je každým dňom viac a viac komplexnejší. Mať prehľad vo všetkých nových frameworkoch, knižniciach, tooloch, best practices a iných vychytávkach by mohlo byť pokojne full-time jobom, a aj tak by som asi nestíhal všetko pokryť. Preto mať zdroj(e), odkiaľ môžem čerpať nové informácie, hlavne tie, čo reálne potrebujem, je na nezaplatenie. Začal som najskôr tým, že som si určil cestu, aby som stále neriešil všetko naraz, a zároveň nikdy dôkladne. Z mojich predošlých článkov si už asi mohol pochopiť, že tou cestou je Vue.js. Dajú sa v ňom riešiť komplexné problémy veľmi jednoduchým spôsobom. A tak, ako o chvíľu Vue, tak aj náš RSHOP, sa dočká veľkého updatu. Pri tejto príležitosti som sa rozhodol napísať novú sériu článkov, ktoré budú popisovať problémy, vychytávky, ale aj nové technológie, na ktoré počas tejto cesty natrafíme.

Vue

V nasledujúcich článkoch budem pracovať na javascriptovej aplikácii vo Vue.js frameworku Nuxt, o ktorom som už písal v mojom zatiaľ poslednom blogu. Cieľom je túto aplikáciu prepojiť s PHP, s ktorým bude komunikovať cez jednoduché API. U nás je preferovaný PHP framework CakePHP, preto použijem ten. Každým článkom budem pridávať novú funkcionalitu, alebo ti ukážem nástroje, ktoré uľahčia development. V prípade, že by ťa niečo konkrétne zaujímalo, neváhaj a napíš mi do komentárov.

Setup prostredia

Dajme tomu, že mám fungl nový počítač. Môže byť nahryznuté jabĺčko. Jediné, čo som stihol nainštalovať je Chrome a editor na programovanie. Moja preferencia je VS Code, fajn je aj Sublime. Na rad teraz prichádza rozbehanie celého development stacku s technológiami, ktoré budem používať. Hmm… Kde začať?

Mac ma predinštalované PHP, mám pocit, že v najnovšej verzii systému to je 7.1. Z osobných dôvodov chcem mať verziu 7.2. Jej inštalácia zaberie trochu googlenia, ale nie je to nič zložité. Jedným príkazom spustím inštaláciu, ďalej povolím modul intl, ktorý CakePHP potrebuje, a PHPčko mám ready.

curl -s https://php-osx.liip.ch/install.sh | bash -s 7.2

Teraz potrebujem nejaký server, na ktorom bude PHPčko bežať. Mám predinštalovaný Apache, ktorý by mohol stačiť, ale chcem si otestovať Nginx. Cez brew nič komplikované.

brew install nginx

Základ by teda bol. V čase budem ešte určite potrebovať nejakú databázu, či už klasiku MySQL, alebo možno aj nejakú NoSQL, ako napríklad MongoDB. Aby som vedel ďalej fungovať, musím nasetupovať ssh kľúče, git, composer… Mal by som mať už všetko, čo treba, tak len spustím príkaz na vytvorenie novej CakePHP aplikácie:

composer create-project --prefer-dist cakephp/app cake_nuxt

Ďalším krokom je inštalácia NodeJs, inicializácia nového JS projektu, inštalácia balíčkov ako Nuxt, express… Celý tento proces sa dá zvládnuť vďaka tréningu za relatívne krátky čas. Ak budú na projekte pracovať viacerí, buď si týmto každý bude musieť tiež prejsť, alebo môže nastať ešte komplikovanejšia situácia. Kolega má napríklad iný operačný systém, alebo všetko nainštalované síce už má, ale staršie verzie. Chýbajú mu určité moduly, niektoré packages z mojej aplikácie mu vôbec nejdú nainštalovať, nemá prakticky šancu spustiť aplikáciu. Nevadí, aktualizuje si všetko na najnovšie verzie. Je to síce cesta, ale napríklad staršie aplikácie, na ktorých pracoval, mu teraz kvôli novším verziám nebežia. A tak sa zabíja drahocenný čas hľadaním odpovedí, setupom a veľakrát až frustráciou nad asi najmenej dôležitou vecou z celého developmentu.

Docker

Mojím cieľom je, aby som mal celú aplikáciu s komplet stackom rozbehanú behom pár minút. Ideálne jedným príkazom. To isté platí pre ostatných ľudí v tíme, nezávisle na tom, aký majú OS. Čo majú nainštalované vo svojich počítačoch a v akých verziách. Nechcem riešiť žiadny zdĺhavý úvodný setup, inštalovať dependencies a riešiť “na mojom počítači to funguje” problémy. Cieľom je, aby jedinou dependency na vývoj mojej aplikácie bol nainštalovaný Docker.

Vysvetľovať dopodrobna, čo to Docker je, asi nie je potrebné. Čo ma hlavne zaujíma je, že môžem vytvárať takzvané kontajnery, ktoré v izolovanom prostredí “zapuzdria” určitú funkcionalitu. Budem mať teda kontajner na čisté PHP, ďalší na Node, potom Nginx, MySQL… Všetky tieto kontajnery musím len správne previazať, aby dokázali medzi sebou komunikovať.

Na zjednodušenie života použijem nástroj na vývoj multi-kontajnerových aplikácií docker-compose. Vytvorím konfiguračný yaml súbor docker-compose.yaml, v ktorom si vypíšem všetky kontajnery, s ktorými budem aktuálne pracovať. Definujem volumes pre logy a zdrojové kódy. Namapujem porty. Celú aplikáciu potom už len spustím príkazom docker-compose up, respektíve ukončím docker-compose down. Na pozadí Docker vytvorí novú sieť – bridge, ktorou budú jednotlivé services komunikovať.

version: "3.5"
services:
  php_fpm:
    build: ./php
    container_name: cake_nuxt_php
    working_dir: /var/www/app
    volumes:
      - ./php/app:/var/www/app:cached

  nginx:
    image: nginx:1.15
    container_name: cake_nuxt_nginx
    ports:
      - "8080:80"
    depends_on:
      - php_fpm
      - node

  node:
    build: ./nodejs
    container_name: cake_nuxt_node
    volumes:
      - ./nodejs/app:/var/www/app
    ports:
      - 3000:3000
      - 9229:9229

Pre PHP a Node používam custom buildy, v ktorých inštalujem všetko, čo potrebujem pre vývoj mojej aplikácie. Je tu aj taká možnosť, že si publishnem tento svoj custom image, ktorý môžem používať potom ako zdroj aj pre iné aplikácie/kontajnery, ale pre lepšiu predstavu si teraz vytvorím vlastné Dockerfiles.

PHP Dockerfile:

#  ./php/Dockerfile
 
FROM php:7.2-fpm
RUN apt-get update && apt-get install -y \
    git \
    libzip-dev \
    zip \
    unzip \
    zlib1g-dev \
    g++
RUN docker-php-ext-configure zip --with-libzip
RUN docker-php-ext-install pdo_mysql zip
 
# intl
RUN apt-get install -y libicu-dev 
RUN docker-php-ext-configure intl
RUN docker-php-ext-install intl
 
# composer
RUN curl --silent --show-error https://getcomposer.org/installer | php && \
    mv composer.phar /usr/local/bin/composer

NodeJS Dockerfile (povieme si k nemu bližšie neskôr):

#  ./nodejs/Dockerfile
 
FROM node:10-alpine
 
WORKDIR /var/www/app
 
COPY ./app/package*.json ./
RUN npm install
 
COPY . .
 
CMD [ "npm", "run", "dev" ]

Nginx

V konfigu poviem Nginx serveru, aby počúval na porte 80, ktorý mám z vonku dostupný cez port 8080. By default budú všetky odkazy smerovať na moju JS aplikáciu, ktorá beží na porte 3000. API odkazy, teda všetky linky s prefixom „/api/*“, budú smerované na PHP aplikáciu, ktorá ako root má index.php súbor v zložke /var/www/app/webroot. Toto viem docieliť cez Reverse Proxy, kde viem jednotlivé endpointy maskovať na dané services.

// ./nginx/conf.d/default.conf
 
server {
    listen  80;
    root /var/www/app/webroot;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
 
    location / {
        try_files $uri @node;  
    }
 
    location /api {
        rewrite ^([^.\?]*[^/])$ $1/ break;
        try_files $uri $uri/ /index.php?q=$uri&$args;
    }
 
    location ~ ^/.+\.php(/|$) {
        fastcgi_pass php_fpm:9000;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
 
    location @node {
        proxy_pass http://node:3000;
    }
}

PHP

Najskôr otestujem, či beží CakePHP aplikácia. Do browsera zadám http://localhost:8080/api. Ak dostanem chybovú hlášku, že chýba Api controller (Fatal error: Class ‘App\Controller\AppController’ not found), alebo menej konkrétnu odpoveď (localhost is currently unable to handle this request), je to viac-menej správne. Nemám len vytvorený controller, respektíve som PHP aplikácii nepovedal, kam má smerovať /api linky.

V zložke ./php/app/src/Controller/ manuálne vytvorím súbor ApiController.php. Ak máš PHP s intl modulom nainštalovaný v tvojom počítači, môžeš nový controller vytvoriť cez terminál príkazom bin/cake bake controller Api. Pre účely testovania nechám v hlavnej index metóde, vypísať jednoduchý JSON s property „message“, ktorú budem chcieť vypísať neskôr aj v Nuxt aplikácii.

<?php
namespace App\Controller;
 
class ApiController extends AppController
{
    public function index() 
    {
        return $this->response->withType('application/json')
            ->withStringBody(json_encode([
                'message' => 'Hello PHP World!'
            ]));
    }
}

Ak všetko beží ako má, na odkaze http://localhost:8080/api by som už mal dostať vyššie definovaný JSON výstup.

NodeJS

Ok, PHPčka na chvíľu stačilo a pôjdem do zaujímavejšej časti – Javascriptu. Chcem riešiť všetko v kontajneroch, ale pre tento úvodný setup potrebujem mať Node aj u seba nainštalovaný. Na každom OS je jeho inštalácia dosť jednoduchá, napríklad na Macu stačí cez brew len spustiť príkaz:

brew install node

Na inštaláciu Nuxt mám viacero možností. Môžem nainštalovať a nasetupovať všetko manuálne alebo spustiť inštalátor cez npx. Veci si chcem čo najviac zjednodušiť, využijem teda tento inštalátor. V zložke pre JS aplikáciu, v mojom prípade ./nodejs, spustím príkaz:

npx create-nuxt-app app

Nuxt install

Pre test, či aplikácia beží ako má, môžem príkazom npm run dev, spustiť Nuxt. Automaticky mám dostupný livereload, simple routovanie, state management, kontrolu a automatické opravovanie kódu. Všetko je celkom customizovateľné, takže ak chcem napríklad použiť inú zložku pre pages, jednoducho si to nastavím v konfigu nuxt.config.js.

Nuxt

Ak sa pozrieš na vygenerované súbory, asi najzaujímavejšie bolo nastavenie express servera, aby spolupracoval s Nuxt middleware:

// ./nodejs/app/server/index.js
 
const express = require('express')
const { Nuxt, Builder } = require('nuxt')
const app = express()
 
// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')
 
async function start() {
    // Init Nuxt.js
    const nuxt = new Nuxt(config)
 
    const { host, port } = nuxt.options.server
 
    // Build only in dev mode
    if (config.dev) {
        const builder = new Builder(nuxt)
        await builder.build()
    } else {
        await nuxt.ready()
    }
 
    // Give nuxt middleware to express
    app.use(nuxt.render)
 
    // Listen the server
    app.listen(port, host)
}
 
start()

Teraz už môžem začať pracovať na aplikácii. Najskôr si pripravím/upravím jednoduchý layout v zložke layouts, s názvom default.vue:

<template>
    <div id="content">
        <nuxt />
    </div>
</template>
 
<script>
// ./nodejs/app/layouts/default.vue
 
export default {
}
</script>

Layout prezentuje HTML kostru stránky a bude neskôr obsahovať kód, ktorý sa vyskytuje na všetkých stránkach. Čiže napríklad hlavička, pätička… Pokojne by som mohol vymazať celý script tag, ale nechám si ho pripravený do budúcna. Ak sú ti známe Vue Single File Components, tak asi jediná novinka tu je tag. Ten si môžeš jednoducho predstaviť ako komponent, ktorý bude generovať obsah jednotlivých stránok.

V zložke pages si teraz vytvorím index.vue – komponent prezentujúci indexovú stránku, ktorú zakomponovaný vue-router automaticky použije.

<template>
    <div>
        <h1>CakePHP + Nuxt</h1>
        <p>{{ message }}</p>
    </div>
</template>
 
<script>
// ./nodejs/app/pages/index.vue
 
export default {
    data() {
        return {
            message: 'Hello JS World!'
        }
    }
}
</script>

Základ k aplikácii by som mal mať ready, môžem pre test ešte pozrieť, či všetko beží. Spustím znovu npm run dev. Aplikácia beží na default porte 3000, takže v browseri zadám link http://localhost:3000/, a ak vidím vypísanú message, všetko je ok.

Cieľom je mať všetko pekne v kontajneroch. V docker-compose.yml už mám pripravený service pre Node a aj nginx mám pripravený. Teraz prichádzajú na rad menšie problémy a ťažké hackovanie, kvôli node_modules zložke.

Problém č.1 – host počítač má operačný systém Mac, kontajnery sú virtualizované Linuxy. Inštalované packages sa nemusia zhodovať a celú zložku nemôžem len tak zdieľať medzi hostom a kontajnerom. Preto si v zložke s Node Dockerfile vytvorím súbor .dockerignore, kam len pridám node_modules zložku, ktorá sa teraz pri builde nedostane do kontajnera.

Problém č.2 – je žiadané, aby spustenie prostredia bolo čo najrýchlejšie. Vytvárať node_modules pri každom spustení kontajnera zbytočne zaberá čas. Pre toto sa odporúča upraviť trocha build a pred kopírovaním zdrojových kódov najskôr vykonať COPY package*.json a hneď na to spustiť npm install. Docker si výstup týchto krokov uloží do cache, a kým nenastane v package*.json zmena, tieto kroky rovno preskočí.

Problém č.3 – prvý build a spustenie kontajnera by mal byť v poriadku. Pri ďalšom jeho spustení, node_modules zložka nebude vďaka kombinácii riešení predošlých problémov, dostupná. Preto si uložím jej obsah v Docker volume, v ktorom môžem ukladať vygenerované súbory. Musím len pridať jeden riadok do môjho docker-compose.yml pre node service.

version: "3.5"
services:
  ...
 
  node:
    build: ./nodejs
    container_name: cake_nuxt_node
    volumes:
      - ./nodejs/app:/var/www/app
      - /var/www/app/node_modules # node_modules volume
    ports:
      - 3000:3000
      - 9229:9229
    depends_on:
      - php_fpm

API

Na komunikáciu s API používam axios, ktorý má plugin priamo do Nuxt.

npm install --save @nuxtjs/axios

Keď chcem pridať nový modul do Nuxt aplikácie, musím ho mať definovaný aj v konfigu:

// ./nodejs/app/nuxt.config.js
 
...
modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios'
],
...

Teraz môžem priamo v komponente pracovať s axios a rovno otestovať komunikáciu s API. Upravím trochu moju indexovú stránku, kde chcem, aby default hodnota message bola prepísaná výstupom z API endpointu:

// ./nodejs/app/pages/index.vue
 
export default {
    data() {
        return {
            message: 'Hello JS World!'
        }
    },
    created() {
        this.$axios
            .$get('/api')
            .then(response => (this.message = response.message))
    }
}

Ak vidím na stránke definovaný message z API, mám vyhraté. Môžem konečne prejsť na zábavnejšiu časť, skutočný development. Ale o tom, a mnohom ďalšom, ti poviem v budúcich častiach. Preto nezabudni sledovať našu FB stránku, aby ti nič neutieklo.

V prípade, že by si nám chcel pomôcť s vývojom ďalšej generácie nášho projektu RSHOP, alebo máš záujem sa od nás niečo priučiť, neváhaj a ozvi sa nám. Ak náhodou na našej kariérnej stránke kariera.riesenia.com nenájdeš svoju vysnívanú pozíciu, napíš nám priamo na náš email joinus@riesenia.com a radi ťa u nás privítame.

+ Diskusia nemá žiadne príspevky