Full Stack kehitys ja CodeSandbox

Kun tehdään Full Stack tyyppistä ohjelmointia, nopeasti herää kysymys “miten saa julkaistua helposti testi version internettiin?”. CodeSandbox on uusi mielenkiintoinen pilvipalvelu, jonka avulla on kätevää ja nopeaa tehdä MERN-tyyppisiä (MongoDb tietokanta, Express webserveri, React käyttöliittymä, Node.js javascript ympäristö) nettisivu sovelluksia. Sivu on heti auki internetiin, ja linkillä voi jakaa helposti lukuoikeuden myös muille kehittäjille. Eli tämän palvelun avulla on helppoa tehdä FullStack tyyppistä websivu kehitystä pienryhmässä. Kaikki näkevät koodin sitä mukaa kun se kehittyy ja web-sivu on aina valmiina testausta varten. Tämä nopeuttaa kehitystä huomattavasti.

Ennen kun aloitetaan koodin kehitys, kannattaa tietenkin luoda itselle tunnukset MongoDB tietokantaan, joka on myös ilmainen pilvipalvelu ja kaikille avoin, testausta ja opettelua varten. https://www.mongodb.com/ Tietokannan käyttö vaatii hieman opettelua, mutta tähän löytyy hyviä ohjeita. Esimerkiksi; https://youtu.be/ofme2o29ngU

MongoDB Compass on apuväline työkaluohjelma, jonka voi ladata ilmaiseksi omalle koneelle. https://www.mongodb.com/try/download/compass

Toinen apuväline ohjelma, jota paljon käytetään FullStack kehityksessä on Postman. Sekin kannattaa ladata ja asentaa omalle kehityskoneelle. https://www.postman.com/downloads/

CodeSandbox:iin voi tutustua ilmaiseksi ja kirjautuminen onnistuu helposti Googlen tai GitHub:n tunnuksilla. https://codesandbox.io/

Kirjautumisen jälkeen, luodaan uusi projekti. Official templates:n alta löytyy esim. Node.js template.

CodeSandbox luo automaattisesti pilveen serverille virtuaalikoneen ja asentaa sinne Node.js:n ja npm:n käyttöä varten. Näin ei tarvita välttämättä omaa serveriä, jos halutaan julkaista oma koodi internettiin. CodeSandbox:n avulla, uusi projekti saa heti uuden web-osoitteen, joka on heti käytettävissä.

Terminalin avulla, voidaan asentaa lisää Node.js javascript paketteja tähän uuteen virtuaalikoneeseen.

Asennetaan esimerkiksi express, mongoose, cors, json ja nodemon paketit.

npm install express
npm install mongoose
npm install cors
npm install json
npm install --save-dev nodemon

Tämän jälkeen, lähdetään kehittämään meidän omaa FullStack sovellusta. Tehdään yksinkertainen TODO-sovellus, joka tallentaa tietoa MongoDB tietokantaan. Uusia rivejä voidaan lisätä, poistaa, muokata.

Sandbox on jo luonut meille automaattisesti “index.js” tiedoston, jota voimme nyt lähteä muokkaamaan.

const express = require("express");
const cors = require("cors");
const app = express();
const port = 3000;

app.use(express.json())       // otetaan JSON tyyppisen tiedon käsittely käyttöön.

const mongoose = require("mongoose");
const mongoDB =
  "mongodb+srv://<OMA_CLUSTER_NIMI_TAHAN>:<OMA_SALASANA>@OMA_CLUSTER.mongodb.net/myFirstDatabase?retryWrites=true&w=majority";
mongoose.connect(mongoDB, { useNewUrlParser: true, useUnifiedTopology: true });

const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open", function () {
  console.log("Tietokanta valmiina ottamaan tietoa vastaan");
});

// scheema
const todoSchema = new mongoose.Schema({
  text: { type: String, required: true },
});

// model
const Todo = mongoose.model("Todo", todoSchema, "todos");

// Routes here... post odottaa saavansa selaimelta tietoa
//                 nyt kun testataan ja kehitetään koodia.    selain voi olla, myös,  postman  tai visual studio coden rest
//                postman on apuväline,  jolla voidaan lähettää tänne  tietoa.
//                                       eli sillä lähetetään  "body"  js
app.post("/todos", async (request, response) => {
  const { text } = request.body;
  const todo = new Todo({
    text: text,
  });
  const savedTodo = await todo.save();
  response.json(savedTodo);
});

// cors - allow connection from different domains and ports
app.use(cors());

// convert json string to json object (from request)
app.use(express.json());

// mongo here...

// todos-route       selaimella GET https://47jhnl-3000.preview.csb.app/todos
app.get("/todos", (request, response) => {
  response.send("Todos");
});

// app listen port 3000
app.listen(port, () => {
  console.log("Kuuntelen porttia 3000");
});

Tämä on hyvin yksinkertainen pohja, jolla voidaan lähteä liikkeelle ja testaamaan toimintaa.

Web-sivu on nyt jo auki internetiin. Sivun osoitteen näkee CodeSandbox:n selain ikkunan osoitesivulta.

Saman osoitteen voi kirjoittaa vaikkapa toiseen internet selaimeen, ja tarkastaa että sivu löytyy internetistä;

Postman on apuväline ohjelma, jonka avulla voidaan testata palvelinpuolen komponenttien toimintaa. Lähetetään kokeilun vuoksi postman ohjelmalla, json muotoinen tekstiviesti POST body:ssä meidän palvelimelle. Valitaan POST viesti, “Body”, tyyppi ja json muotoinen tieto. Sitten kirjoitetaan viesti;

{
   "text" : "Muista opetella Javascript-ohjelmointia!"
}

Nyt nähdään, että meille tulee 200 OK viesti takaisin. Seuraavaksi voidaan käyttää toista apuväline ohjelmaa, “MongoDC Compass”, jolla voidaan tarkastaa, että tietokantaan tallentui tekstimuotoinen tieto.

Kyllä, tieto tallentuu MongoDB tietokantaan oikein.

Seuraavaksi muokataan vielä hiukan reittejä, jotta myös tiedon lukeminen MongoDV tietokannasta onnistuu:

app.get('/todos', async (request, response) => {
  const todos = await Todo.find({})
  response.json(todos)
})

app.get('/todos/:id', async (request, response) => {
  const todo = await Todo.findById(request.params.id)
  if (todo) response.json(todo)
  else response.status(404).end()
})

app.delete('/todos/:id', async (request, response) => {
  const deletedTodo = await Todo.findByIdAndRemove(request.params.id)
  if (deletedTodo) response.json(deletedTodo)
  else response.status(404).end()
})

Tämän jälkeen ruvetaan tekemään UI-käyttöliittymä osuutta. Luodaan uusi kansio “kayttoliittyma”.

Ja “kayttoliittyma” kansion sisälle, luodaan uusi tiedosto “index.html”.

<!DOCTYPE html>
<html>
     <link rel="icon" href="data:;base64,iVBORw0KGgo=">
<head>
  <meta charset="UTF-8">
  <title>FullStack-esimerkkisovellus</title>
  <link rel="stylesheet" href="styles.css">
  <script src="code.js"></script>
</head>
<body onload="init()">
  <div id="container">
    <h1>Tehtävälista</h1>
    <input type="text" id="newTodo"/>
    <button onclick="addTodo()">Lisää</button>
    <ul id="todosList"></ul>
    <p id="infoText"></p>
  </div>
</body>
</html>

Seuraavaksi luodaan vielä uusi tiedosto “styles.css”, jossa voidaan määritellä käyttöliittymän tyyli.

body {
  font-family: "Oswald", sans-serif;
}

#container {
  width: 420px;
  background-color: #fff0f5;
  border-radius: 5px;
  padding: 0px 15px 0px 15px;
  margin: 10px auto;
  border: 5px solid #ffb6c1;
}

h1 {
  background-color: #ff66cc;
  text-align: center;
  color: white;
  padding: 5px;
}

ul {
  list-style-type: square;
  margin: 20px 0 20px 0;
}

li {
  font-style: oblique;
  font-size: 19px;
}

input[type="text"] {
  border: 1px solid #ccc;
  padding: 5px;
  width: 300px;
  font-size: 15px;
}

button {
  border-radius: 10px;
  border: 5px solid #ff69b4;
  margin-left: 15px;
  padding: 5px 15px;
  font-size: 15px;
  cursor: pointer;
}

.delete {
  cursor: pointer;
  color: red;
  font-weight: bold;
}

Nyt voidaan lähteä kehittämään varsinaista käyttöliittymää web-sovellukselle.

Tehdään vielä yksi tiedosto, “code.js” samaan kansioon, missä “index.html” ja “styles.css”.

function init() {
  let infoText = document.getElementById('infoText')
  infoText.innerHTML = 'Ladataan tehtävälista palvelimelta, odota...'
  //loadTodos()
  //loadTodos()           // otetaan tämä käyttöön, kun alkuvaih. testaus tehty
}

async function loadTodos() {
  let response = await fetch('http://localhost:3000/todos')
  let todos = await response.json()
  console.log(todos)
  //showTodos(todos)
  //showTodos(todos)      // otetaan tämä käyttöön, kun alkuvaih. testaus tehty
}

Jotta voidaan tarjota myös staattista html sisältöä, Node.js Express web-serverin kautta, meidän tulee määrittää “index.js” tiedostossa, kansio jossa ne sijaitsevat.

Lisätään “index.js” tiedostoon;

// Tarjotaan, Kayttöliittymä puolta tarjolle, Express:llä.
app.use(express.static('kayttoliittyma'))

Nyt kun kokeillaan taas selaimella yhdistää meidän uuteen hienoon web-sovelluksen osoitteeseen, meillä avautuu jo ensimäinen versio käyttöliittymästä.

Seuraavaksi lisätään “code.js” tiedostoon lisää koodia, jotta voidaan näyttää tietokantaan tallennettuja tietoja html muodossa LI-elementteinä.

function createTodoListItem(todo) {
  // luodaan uusi LI-elementti
  let li = document.createElement('li')
    // luodaan uusi id-attribuutti
  let li_attr = document.createAttribute('id')
    // kiinnitetään tehtävän/todon id:n arvo luotuun attribuuttiin 
  li_attr.value= todo._id
    // kiinnitetään attribuutti LI-elementtiin
  li.setAttributeNode(li_attr)
    // luodaan uusi tekstisolmu, joka sisältää tehtävän/todon tekstin
  let text = document.createTextNode(todo.text)
    // lisätään teksti LI-elementtiin
  li.appendChild(text)
    // luodaan uusi SPAN-elementti, käytännössä x-kirjan, jotta tehtävä saadaan poistettua
  let span = document.createElement('span')
    // luodaan uusi class-attribuutti
  let span_attr = document.createAttribute('class')
    // kiinnitetään attribuuttiin delete-arvo, ts. class="delete", jotta saadaan tyylit tähän kiinni
  span_attr.value = 'delete'
    // kiinnitetään SPAN-elementtiin yo. attribuutti
  span.setAttributeNode(span_attr)
    // luodaan tekstisolmu arvolla x
  let x = document.createTextNode(' x ')
    // kiinnitetään x-tekstisolmu SPAN-elementtiin (näkyville)
  span.appendChild(x)
    // määritetään SPAN-elementin onclick-tapahtuma kutsumaan removeTodo-funkiota
  span.onclick = function() { removeTodo(todo._id) }
    // lisätään SPAN-elementti LI-elementtin
  li.appendChild(span)
    // palautetaan luotu LI-elementti
    // on siis muotoa: <li id="mongoIDXXXXX">Muista soittaa...<span class="remove">x</span></li>
  return li
}

Kun ollaan edetty kehitysvaiheessa eteenpäin ja tarkastettu että tietokannan kirjoittaminen ja lukeminen onnistuu. Seuraavaksi voidaan poistaa kommentit, eli otetaan käyttöön “loadTodos(todos)” funktio. sekä showTodos() funktio, hieman alempana.

Ja lisätään tämä uusi showTodos(todos) funktio “code.js” tiedoston loppuun.

function showTodos(todos) {
  let todosList = document.getElementById('todosList')
  let infoText = document.getElementById('infoText')
  // no todos
  if (todos.length === 0) {
    infoText.innerHTML = 'Ei tehtäviä'
  } else {    
    todos.forEach(todo => {
        let li = createTodoListItem(todo)        
        todosList.appendChild(li)
    })
    infoText.innerHTML = ''
  }
}

Nyt kun päivitetään selain, meillä tulostuu tietokannan tietoa ruudulle mukavammin.

Lisää ja poista napit eivät nyt vielä toimi. Mutta ne saadaan toimimaan, kun lisätään vielä niille omat funktiot “code.js” tiedoston loppuun.

addTodo() funktio:

async function addTodo() {
  let newTodo = document.getElementById('newTodo')
  const data = { 'text': newTodo.value }
  const response = await fetch('/todos', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  })
  let todo = await response.json()
  let todosList = document.getElementById('todosList')
  let li = createTodoListItem(todo)
  todosList.appendChild(li)

  let infoText = document.getElementById('infoText')
  infoText.innerHTML = ''
  newTodo.value = ''
}

removeTodo(id) funktio:

async function removeTodo(id) {
  const response = await fetch('/todos/'+id, {
    method: 'DELETE'
  })
  let responseJson = await response.json()
  let li = document.getElementById(id)
  li.parentNode.removeChild(li)

  let todosList = document.getElementById('todosList')
  if (!todosList.hasChildNodes()) {
    let infoText = document.getElementById('infoText')
    infoText.innerHTML = 'Ei tehtäviä'
  }
}

Eli nyt meidän “/kayttoliittyma/code.js” tiedosto näyttää lopullisesti tällaiselta:

function init() {
  let infoText = document.getElementById("infoText");
  infoText.innerHTML = "Ladataan tehtävälista palvelimelta, odota...";
  //loadTodos()
  loadTodos(); // otetaan tämä käyttöön, kun alkuvaih. testaus tehty
}

async function loadTodos() {
  console.log("Konsoliin pitaisi tulla, tietokannan tietoa: ");
  
  let response = await fetch("/todos");
  let todos = await response.json();

  console.log(todos);
  //showTodos(todos)
  showTodos(todos); // otetaan tämä käyttöön, kun alkuvaih. testaus tehty
}

function createTodoListItem(todo) {
  // luodaan uusi LI-elementti
  let li = document.createElement("li");
  // luodaan uusi id-attribuutti
  let li_attr = document.createAttribute("id");
  // kiinnitetään tehtävän/todon id:n arvo luotuun attribuuttiin
  li_attr.value = todo._id;
  // kiinnitetään attribuutti LI-elementtiin
  li.setAttributeNode(li_attr);
  // luodaan uusi tekstisolmu, joka sisältää tehtävän/todon tekstin
  let text = document.createTextNode(todo.text);
  // lisätään teksti LI-elementtiin
  li.appendChild(text);
  // luodaan uusi SPAN-elementti, käytännössä x-kirjan, jotta tehtävä saadaan poistettua
  let span = document.createElement("span");
  // luodaan uusi class-attribuutti
  let span_attr = document.createAttribute("class");
  // kiinnitetään attribuuttiin delete-arvo, ts. class="delete", jotta saadaan tyylit tähän kiinni
  span_attr.value = "delete";
  // kiinnitetään SPAN-elementtiin yo. attribuutti
  span.setAttributeNode(span_attr);
  // luodaan tekstisolmu arvolla x
  let x = document.createTextNode(" x ");
  // kiinnitetään x-tekstisolmu SPAN-elementtiin (näkyville)
  span.appendChild(x);
  // määritetään SPAN-elementin onclick-tapahtuma kutsumaan removeTodo-funkiota
  span.onclick = function () {
    removeTodo(todo._id);
  };
  // lisätään SPAN-elementti LI-elementtin
  li.appendChild(span);
  // palautetaan luotu LI-elementti
  // on siis muotoa: <li id="mongoIDXXXXX">Muista soittaa...<span class="remove">x</span></li>
  return li;
}

function showTodos(todos) {
  let todosList = document.getElementById("todosList");
  let infoText = document.getElementById("infoText");
  // no todos
  if (todos.length === 0) {
    infoText.innerHTML = "Ei tehtäviä";
  } else {
    todos.forEach((todo) => {
      let li = createTodoListItem(todo);
      todosList.appendChild(li);
    });
    infoText.innerHTML = "";
  }
}

async function addTodo() {
  let newTodo = document.getElementById("newTodo");
  const data = { text: newTodo.value };
  const response = await fetch("/todos", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
  });
  let todo = await response.json();
  let todosList = document.getElementById("todosList");
  let li = createTodoListItem(todo);
  todosList.appendChild(li);

  let infoText = document.getElementById("infoText");
  infoText.innerHTML = "";
  newTodo.value = "";
}

async function removeTodo(id) {
  const response = await fetch("/todos/" + id, {
    method: "DELETE",
  });
  let responseJson = await response.json();
  let li = document.getElementById(id);
  li.parentNode.removeChild(li);

  let todosList = document.getElementById("todosList");
  if (!todosList.hasChildNodes()) {
    let infoText = document.getElementById("infoText");
    infoText.innerHTML = "Ei tehtäviä";
  }
}

Ja nyt voidaan lisätä ja poistaa “Todo-tehtäviä”, käyttöliittymästä.

Näin helppoa on FullStack kehitys, CodeSandbox:n avulla. Koodi on jatkuvasti testattavissa.

Kun painetaan Ctrl + s näppäinyhdistelmää, eli tallennetaan muutokset, CodeSandbox käynnistää sovelluksen automaattisesti uudelleen. Joskus koodi voi kuitenkin jäädä jumiin, jos ei ole huolehdittu virheiden käsittelystä Node.js tiedostoissa, tällöin voi joutua itse resetoimaan sovelluksen.

Oikean yläkulman “Share” valikosta, voidaan helposti kopioida linkki, jonka voi jakaa toisille.

Copy Link painikkeesta kopioituu linkki, “Anonymous” käyttäjälle. Sen jos jakaa esim. kaverille, hän pääsee testaamaan ja kokeilemaan helposti ja pysty lukemaan koodia, mutta hän ei voi tehdä muutoksia. Kutsumalla tiimiin jäseniä, voi antaa myös editointi oikeuksia muille “Tiimin jäsenille”.

Visual Studio Code editoriin, jota paljon FullStack kehityksessä käytetään, voidaan asentaa laajennus, jonka avulla voidaan suoraan kirjoittaa koodia CodeSandbox-pilvipalveluun. Tämä myös nopeuttaa kehitystä ja voi käyttää tuttua editoria koodin kirjoittamiseen.