Alrededor de 2014 se empezaba a sentir la utilidad que prestaba el desarrollo con Javascript con un papel más protagónico cubriendo diversos aspectos. En ese momento, era popular en esa comunidad de entusiastas, un proyecto Fullstack
combinando tecnologías como MongoDB
para contar con una base de datos que utilizaba de modo natural JSON
, Express
para el servidor web, AngularJS
en su primera versión para la vista y por supuesto Node.js
, usándose el acrónimo MEAN
(MongoDB + Express + AngularJS + Node.js). En el escenario propuesto que se ha venido presentando, en lugar de Express
es posible usar Restana
, en lugar de Angular
usamos RiotJS
y la base de datos puede variar, iniciándo con una tecnología sencilla como LokiJS
que no requiere instalación de un servidor y tiene semejanzas con la API de MongoDB
.
Si se pregunta por qué usar Restana o Riot, la respuesta simple es que al analizar o armar tu propio rompecabezas sientes que una pieza no encaja hasta que encuentras la apropiada para ese rompecabezas. Por supuesto que existen criterios técnicos, logísticos, antecedentes u otros, pero eso se presta para demasiadas opiniones al confrontarlo con otras tecnologías muy interesantes y no para lo que estás logrando en tu propio recorrido de aprendizaje o en un proyecto como OnMind. Dado que Restana
y Riot
son librerías sencillas de implementar respecto a otras, puedes darte la oportunidad de tomar tu propia opinión. Pienso que en éste contexto suelo buscar algo que coincide con una frase atribuida a Albert Einstein que dice:
“hazlo tan simple como sea posible, pero no más”
Apliquemos ahora conocimientos adquiridos si ya los tienes o se ha seguido la serie de artículos propuestos por OnMind (Enfoques para Programar,Javascript, Javascript bajo Node.js, RiotJS). El proyecto corresponderá a un directorio de contactos sencillo.
Procura evitar copiar y pegar. Quizás copia los archivos de configuración pero en cuanto al código es importante reconocer cada línea de código como si la hubieses escrito aunque el código ya se encuentre listo. Re-escribir o digitar código de éste proyecto de práctica puede porporcionar una manera apropiada de asimilar el aprendizaje sobre éste tema.
Para comprender mejor la disposición de nuestros archivos, tendríamos lo siguiente:
📂 gui
├── src
│⋅⋅⋅⋅⋅⋅├── gui.js
│⋅⋅⋅⋅⋅⋅├── index.html
│⋅⋅⋅⋅⋅⋅└── my-data.riot
├── package.json
└── webpack.config.js
📂 api
├── src
│⋅⋅⋅⋅⋅⋅├── api.js
│⋅⋅⋅⋅⋅⋅└── dbaccess.js
├── run
│⋅⋅⋅⋅⋅⋅├── server.bundle.js
│⋅⋅⋅⋅⋅⋅├── database.json
│⋅⋅⋅⋅⋅⋅└── assets
│⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅├── index.html
│⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅└── dist
│⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅└── app.bundle.js
├── package.json
└── webpack.config.js
Básicamente tendríamos dos proyectos. Nuestro proyecto front-end se llamará
gui
y nuestro proyecto back-end se llamaráapi
. El “script” principal degui
se distribuirá con el nombreapp.bundle.js
, mientras el “script” principal deapi
se distribuirá con el nombreserver.bundle.js
. Los archivos estáticos distribuibles degui
quedarían dentro de la carpetaapi/run/assets
.
Para la base de datos se usará en este ejercicio LokiJS
. Además, dado que procesaremos peticiones en JSON
se necesitará para ello el módulo body-parser
. Estos módulos se podrían instalar con npm install -S lokijs body-parser
, sin embargo, esta vez tendrémos preparado nuestro archivo package.json
para api
, así:
{
"scripts": {
"start": "webpack --mode development",
"build": "webpack --mode production",
"serve": "nodemon run/server.bundle.js"
},
"dependencies": {
"body-parser": "^1.19.0",
"lokijs": "^1.5.7",
"restana": "^3.3.2",
"serve-static": "^1.14.1"
},
"devDependencies": {
"@babel/core": "^7.6.3",
"@babel/preset-env": "^7.6.3",
"babel-loader": "^8.0.6",
"nodemon": "^1.19.4",
"webpack": "^4.41.1",
"webpack-cli": "^3.3.9",
"webpack-node-externals": "^1.7.2"
}
}
Para instalar los paquetes respectivos simplemente nos ubicamos en la carpeta del proyecto
api
y ejecutamosnpm install
.
Del mismo mode tendrémos preparado nuestro package.json
para gui
, así:
{
"scripts": {
"start": "webpack --mode development",
"build": "webpack --mode production"
},
"dependencies": {
"riot": "^4.6.5"
},
"devDependencies": {
"@riotjs/compiler": "^4.5.1",
"@riotjs/webpack-loader": "^4.0.1",
"copy-webpack-plugin": "^5.0.4",
"webpack": "^4.41.1",
"webpack-cli": "^3.3.9"
}
}
Para instalar los paquetes respectivos simplemente nos ubicamos en la carpeta del proyecto
gui
y ejecutamosnpm install
.
El archivo de configuración webpack.config.js
para api
tendrá el siguiente contenido:
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
entry: './src/api.js',
output: {
path: path.resolve(__dirname, 'run'),
filename: 'server.bundle.js'
},
target: 'node',
externals: [nodeExternals()],
node: {
console: false,
__dirname: false,
__filename: false
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
}
}
]
}
}
El archivo de configuración webpack.config.js
para gui
tendrá el siguiente contenido:
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry: './src/gui.js',
output: {
path: path.join(__dirname, '..', 'api', 'run', 'assets', 'dist'),
filename: 'app.bundle.js'
},
module: {
rules: [
{
test: /\.riot$/,
exclude: /node_modules/,
use: [{
loader: '@riotjs/webpack-loader',
options: { hot: false }
}]
}
]
},
plugins: [
new CopyWebpackPlugin([
{ from: './src/index.html', to: '..' } // output.path & back to 'assets'
])
]
}
Comenzando con el back-end, para api
tendremos como “script” principal api.js
que gestiona el servidor web y los micro-servicios con el siguiente código:
const files = require('serve-static');
const restana = require('restana');
const bodyParser = require('body-parser');
const db = require('./dbaccess');
const service = restana();
const port = 3000;
service.use(bodyParser.json());
service.use(bodyParser.urlencoded({ extended: true }));
service.use(files(__dirname + '/assets'));
service.post('/mydata/', db.insert);
service.get('/mydata/', db.list);
service.delete('/mydata/:id', db.remove);
service.start(port).then((server) => {
console.log(`Service listening at ${port}`);
});
Nótese que aquí es se requiere un archivo adicional para el proyecto con nombre
dbaccess.js
, el cual gestiona los datos usandoLokiJS
, como se puede observar a continuación:
const path = require('path');
const loki = require('lokijs');
const dbFile = path.join(__dirname, 'database.json');
const db = new loki(dbFile, {
autoload: true,
autosave: true,
autosaveInterval: 4000
});
const save = (data) => {
var count = 0
var result = {}
try {
var mydata = db.getCollection('mydata');
count = mydata.count();
console.info(`Data in ${dbFile}: ${count}`);
mydata.on('insert', (row) => { row.id = row.$loki; }); // set auto id
result = mydata.insert(data);
db.saveDatabase();
}
catch (e) {
if (count == 0) { // Create the collection the first time (auto-create)
var mydata = db.addCollection('mydata');
mydata.on('insert', (row) => { row.id = row.$loki; }); // set auto id
result = db.getCollection('mydata').insert(data);
}
else {
console.error(e.message,count,result)
throw e
}
}
return result;
}
exports.insert = async function (req, res, next) {
var data = req.body.data;
var result = save(data);
res.send(result);
}
exports.list = async function (req, res, next) {
try {
var result = db.getCollection('mydata');
res.send(result.data);
}
catch (e) {
res.send({ success: false, message: 'Not data found, try insert!', extra: e.message });
}
}
exports.remove = async function (req, res, next) {
var id = req.params.id;
var data = db.getCollection('mydata');
var result = data.where((row) => { return row.id == id }); // or data.find({ 'id': id })
data.remove(result);
res.send({ success: true, message: `${id} removed!` });
}
Nótese que se usa
exports
enNode.js
para indicar que una función en particular es de acceso público y puede ser invocada como una librería. También podrían listarse al final tales funciones expresándose comoexports { insert, list, remove }
Podemos lanzar nuestro servidor ubicándonos enapi
, primero ejecutamosnpm start
y luegonpm run serve
(o,cd run
ynode server.bundle.js
). Es posible que en tiempo de desarrollo se necesiten abrir otras sesiones de terminal.
Continuando con el front-end, para gui
tendremos el archivo index.html
usado en un proyecto anterior pero en la nueva ubicación, es decir, la siguiente plantilla:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Riot Component</title>
<!-- Semantic-UI Styles -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css" rel="stylesheet" media="screen">
<!-- HighlightsJS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.9/styles/ocean.min.css">
</head>
<body>
<div id="app"></div>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.9/highlight.min.js"></script>
<script defer src="dist/app.bundle.js"></script>
</body>
</html>
El “script” principal gui.js
que invoca nuestro componente web tendrá siguiente código:
import { component } from 'riot'
import Tag from './my-data.riot'
component(Tag)(document.getElementById('app'), {})
Veamos ahora el contenido del archivo my-data.riot
:
<my-data>
<div class={ state.color }>
<br/>
<div class="ui text container">
<h1 class="ui { state.color == 'dark' ? 'yellow' : 'blue' } header">
<i class="address book icon"></i>
<div class="content">
My Directory
</div>
</h1>
<form class="ui form" onsubmit={ onSend } autocomplete="off">
<div class="field">
<div class="two fields">
<div class="field">
<input type="text" name="name" placeholder="First Name" required>
</div>
<div class="field">
<input type="text" name="lastname" placeholder="Last Name">
</div>
</div>
</div>
<button class="ui right floated icon button">
<i class="save blue icon"></i>
</button>
<button class="ui right floated icon button" onclick={ onList }>
<i class="refresh icon"></i>
</button>
</form>
<div class="ui toggle checkbox">
<input type="checkbox" onchange={ onCheck }>
<label><i class="adjust orange icon"></i></label>
</div>
<br/>
<div class="ui basic tiny label">
<i class="world icon"></i> { state.baseURI }
</div>
<div class="ui placeholder segment" id="holder" if={ (state.status == 0) }>
<div class="ui icon header">
<a onclick={ onList }><i class="search blue link icon"></i></a>
Try it!
</div>
</div>
<table class="ui { state.color == 'dark' ? 'inverted' : '' } selectable unstackable table" if={ (state.status == 1) }>
<thead>
<tr>
<th>Name</th>
<th>Last Name</th>
<th></th>
</tr>
</thead>
<tbody>
<tr each={ row in state.data }>
<td>{ row["name"] }</td>
<td>{ row["lastname"] }</td>
<td class="collapsing">
<button class="ui basic icon button" onclick={ onRemove } data-id={ row["id"] }>
<i class="trash red icon"></i>
</button>
</td>
</tr>
</tbody>
</table>
<pre if={ (state.status == -1) }>
<code style="max-height: 300px; overflow-y: scroll;">{ state.code }</code>
</pre>
</div>
</div>
<style>
.clear {
background-color: white;
color: black;
height: 100vh;
}
.dark {
background-color: dimgray;
color: snow;
height: 100vh;
}
</style>
<script>
export default {
state: {
status: 0, // 0 => initial, 1 => ok, -1 => error
name: 'john',
color: 'dark',
data: [],
baseURI: 'http://localhost:3000/mydata/'
},
onMounted() { // When component is mounted or created
this.update()
},
onSend(e) {
e.preventDefault()
this.update({ status: 1, code: '...' }) // update or render again HTML (DOM)
let form = document.forms[0] // get the first form or unique
let name = form['name'].value
let lastname = form['lastname'].value
if (!name) {
this.update({ code: 'Name wrong!' })
return
}
let data = {
"name": name,
"lastname": lastname
}
let options = {
method: 'POST',
body: JSON.stringify({ "data": data }),
headers: { 'Content-Type': 'application/json' }
}
fetch(this.state.baseURI, options)
.then(res => res.json())
.then(data => {
form['name'].value = null
form['lastname'].value = null
this.onList()
})
.catch(err => {
this.update({ status: -1, code: `${this.state.baseURI} => ${err.message}` })
hljs.initHighlighting()
});
},
onList(e) {
if (e) e.preventDefault() // this "if" is because can be called whithout "e"
this.update({ status: 1, code: '...' }) // update or render again HTML (DOM)
fetch(this.state.baseURI)
.then(res => res.json())
.then(data => {
this.update({ code: JSON.stringify(data, null, ' '), data: data })
})
.catch(err => {
this.update({ status: -1, code: `${this.state.baseURI} => ${err.message}` })
hljs.initHighlighting()
});
},
onRemove(e) {
e.preventDefault()
let target = e.target
if (target.tagName == 'I')
target = e.target.parentNode
// Get the "id" from element or target attribute
let id = target.getAttribute('data-id')
fetch(this.state.baseURI + id, { method: 'DELETE' })
.then(res => res.json())
.then(data => {
this.onList()
})
.catch(err => {
this.update({ status: -1, code: `${this.state.baseURI} => ${err.message}` })
hljs.initHighlighting()
});
},
onCheck(e) {
if (this.state.color == 'clear')
this.state.color = 'dark'
else
this.state.color = 'clear'
this.update()
}
}
</script>
</my-data>
Ahora podemos generar los archivos distribuibles de
gui
ejecutandonpm start
y puedes consultar en el navegador la dirección respectiva, por ejemplo:localhost:3000
(asegúrate que este operandoapi
)
Vamos a reorganizar por piezas o partes de nuestra vista destinando un sub-componente para el formulario (my-form.riot
) y un sub-componente para la tabla (my-table.riot
), quedando nuestra carpeta gui
de la siguiente manera:
El código para my-form.riot
correspondería al siguiente:
<my-form>
<form class="ui form" onsubmit={ props.onsend } autocomplete="off">
<div class="field">
<div class="two fields">
<div class="field">
<input type="text" name="name" placeholder="First Name" required>
</div>
<div class="field">
<input type="text" name="lastname" placeholder="Last Name">
</div>
</div>
</div>
<button class="ui right floated icon button">
<i class="save blue icon"></i>
</button>
<button class="ui right floated icon button" onclick={ props.onlist }>
<i class="refresh icon"></i>
</button>
</form>
</my-form>
El código para my-table.riot
correspondería al siguiente:
<my-table>
<table class="ui { props.color == 'dark' ? 'inverted' : '' } selectable unstackable table">
<thead>
<tr>
<th>Name</th>
<th>Last Name</th>
<th></th>
</tr>
</thead>
<tbody>
<tr each={ row in props.data }>
<td>{ row["name"] }</td>
<td>{ row["lastname"] }</td>
<td class="collapsing">
<button class="ui basic icon button" onclick={ props.onremove } data-id={ row["id"] }>
<i class="trash red icon"></i>
</button>
</td>
</tr>
</tbody>
</table>
</my-table>
Nuestro achivo my-data.riot
se vería afectado en lo correspondiente a la plantilla (incluso, hasta un par de líneas del “script”), mientras la mayor parte del contenido del “script” queda intacta. Básicamente las líneas de la parte superior quedan de la siguente manera:
<my-data>
<div class={ state.color }>
<br/>
<div class="ui text container">
<h1 class="ui { state.color == 'dark' ? 'yellow' : 'blue' } header">
<i class="address book icon"></i>
<div class="content">
My Directory
</div>
</h1>
<my-form onsend={ onSend } onlist={ onList } />
<div class="ui toggle checkbox">
<input type="checkbox" onchange={ onCheck }>
<label><i class="adjust orange icon"></i></label>
</div>
<br/>
<div class="ui placeholder segment" id="holder" if={ (state.status == 0) }>
<div class="ui icon header">
<a onclick={ onList }><i class="search blue link icon"></i></a>
Try it!
</div>
</div>
<my-table color={ state.color } data={ state.data } onremove={ onRemove } if={ (state.status == 1) } />
<pre if={ (state.status == -1) }>
<code style="max-height: 300px; overflow-y: scroll;">{ state.code }</code>
</pre>
</div>
</div>
<style>
.clear { background-color: white; color: black; height: 100vh; }
.dark { background-color: dimgray; color: snow; height: 100vh; }
</style>
<script>
import MyForm from './my-form.riot'
import MyTable from './my-table.riot'
export default {
components: { MyForm, MyTable },
... // same code inside of "export default"
}
</script>
</my-data>
Cuida de conservar el código que no se ve impactado (que se encuentra dentro
export default
), puesto que sigue como estaba. Se puede notar que se presenta una plantilla de una mejor manera y se pueden reutilizar esas partes en otras funciones sobre esos datos, por ejemplo, si se pensara en implementar la actualización de datos (actualmente se usaría eliminar y volver a insertar).
Recuerda volverlo a generar con npm start
y que el servicio de api
se encuentre disponible. Luego puedes consultar en el navegador la dirección respectiva, por ejemplo: localhost:3000
© 2019 by César Arcila