Básicamente, si estás iniciándote en la programación web o ya has adquirido fundamentos en javascript
te resultará natural poner en práctica lo aprendido, verás que Riot 5
es una librería para la UI
(Interfaz de Usuario) con la que puedes conectar rapidamente suavizando la frustración como iniciado o programador interesado, en palabras más sofisticadas, menor curva de aprendizaje al guardar aspectos cercanos al estándar. Pienso que después de tener nociones y alguna practica mínima con HTML
y javascript
(2015+) es interesante conocer esta librería, incluso hasta puedes quedar conectado con ella si gustas de su sencillez, pequeño tamaño y su velocidad de respuesta.
Como desarrollador, llegué a utilizar la primera versión de Angular
, luego un poco de las nuevas versiones en pruebas de concepto con Ionic
. Estuve usando algunos meses ReactJS
y fue divertido pero en ese entonces su licencia era algo peculiar (poco conveniente para algunos proyectos). Le di un vistazo a Vue
, y aunque veía similitudes con Angular
, en ese momento no me hacía a la idea el tener que usar plantillas con un prefijo (v-
). Cuando encontré Riot
fué tan simple y natural que me arriesgué entonces a darle la oportunidad hasta su evolución a la versión 5, gracias a Gianluca Guarini por su contribución. Por supuesto, Riot
es similar a Vue
en varios aspectos y ambas librerías corresponden a mis habilidades.
En la plataforma OnMind (de la que soy autor) el camino es bien definido, al menos para la versión actual se ha establecido la programación de nuestros componentes visuales ante todo bajo javascript
, y si se pregunta por alguna librería soportamos Riot 5
como “first-class” (de primera clase) en nuestros componentes construidos para un software que es orientado principalmente a la gestión interna de negocios bajo parámetros propios. Incluso para integración nativa con móviles se puede combinar con Capacitor
. Mientras alguien puede encontrarse en una investigación sobre tecnologías similares (como React
, Angular
, Vue
u otra), esté comenzando a aprender del tema o tome sus opiniones, para OnMind es un hecho y camino elegido en nuestra primera versión, estando abierto a revisión en futura versión, que no ocupa tiempo por el momento.
El video ilustra ejemplo que se encuentra en artículo sobre Capacitor + Riot
Si pensamos en un botón, que se visualiza en una aplicacion o una página web, tendriamos una etiqueta como la siguiente:
<button style="color: blue" onsubmit="alert('Hello')">Press</button>
Se puede observar que:
/
), es decir: <button></button>
style
onsubmit
HTML
, en este caso el texto: Press
Comprendiendo esto se construye una plantilla con las etiquetas predefinidas para
HTML
. Este mismo principio se aplica a componentes web que pueden verse como etiquetas pesonalizadas y pueden tener atributos, eventos y contenido (o subcomponentes).
Riot usa una plantilla HTML, en la parte inferior del archivo puede incorporar
<style></style>
y<script></script>
Ahora bien, ¿Si Riot 5
es cercano al estándar HTML
, cual es la diferencia o qué agrega que requiera aprenderse?. En el sentido esencial de las cosas, citaría en principio sólo 3 aspectos:
{}
, las cuales son utilizadas para mostrar contenido de modo dinámico, por ejemplo un título de manera programada en una página bilingue.if
para evaluar si una etiqueta HTML
se muestra o no conforme a una expresión.each
para procesar de modo dinámico datos usados frecuentemente en listas, tablas, opciones de selección o similar.Adicionalmente, se requieren comprender aspectos como la noción de componentes web (que se presentan como etiquetas personalizadas y son piezas para la interfaz visual), el ciclo de vida (eventos para gestión del DOM de HTML), incorporación de elementos o subcomponentes en ranuras (slots), así como algo de estilos (CSS3). Presento esta guía esencial a mi manera, espero resulte de gran ayuda y agrado si suena de tu interés probar o iniciarte con RiotJS
.
<!DOCTYPE html>
<html>
<head>
<title>Riot 5 - Hi there!</title>
</head>
<body>
<my-tag></my-tag>
<script src="https://cdn.jsdelivr.net/npm/riot@4/riot+compiler.min.js"></script>
<script src="my-tag.riot" type="riot"></script>
<script type="module">
(async function main() {
await riot.compile()
riot.mount('my-tag')
}())
</script>
</body>
</html>
Básicamente, se invoca primero al compilador interactivo de
Riot
usandoriot+compiler.min.js
.
Al crearse un componente, por ejemplo un archivomy-tag.riot
, se incluye comoscript
y puede usarse una estructura cercana al estándarHTML
, como se verá a continuación.
<my-tag>
<h3>Hi there!</h3>
This is { state.name }
<script>
export default {
state: {
name: 'My first tag'
}
}
</script>
</my-tag>
Para propiedades internas o alterables se usa la palabra reservada
state
, mientras que para recibir propiedades se usa la palabra reservadaprops
, por ejemplo:
<my-tag title="Welcome"></my-tag>
En este caso se recibiría “Welcome” en
props.title
, valor que se pasa al componente y que no es alterable dentro de éste. Si bien, se pueden usar expresiones dinámicas para enviar el título pero eso sería otra cosa.
Si tienes conocimientos en Vue 3, a continuación se presenta una tabla comparativa esencial de estas tecnologías para la UI.
Concepto | Riot | Vue |
---|---|---|
Uso de expresiones | { } |
{{ }} |
Condicional | if="..." |
v-if="..." |
Ciclo o Iterador | each="..." |
v-for="..." |
Eventos HTML | onclick="..." |
@click="..." |
Usa export default |
Yes | Yes |
Evento onBeforeMount |
Yes | Yes |
Evento onMounted |
Yes | Yes |
Evento onBeforeUpdate |
Yes | Yes |
Evento onUpdated |
Yes | Yes |
Evento onBeforeUnmount |
Yes | Yes |
Evento onUnmounted |
Yes | Yes |
Evento onErrorCaptured |
No | Yes |
Evento shouldUpdate |
Yes | No |
Método this.update() |
Yes | No |
Elemento components |
Yes | Yes |
Vue posee más características que Riot actualmente, también porque Riot busca brindar una mayor simplicidad.
Por ejemplo, Vue incorporav-model
para componentes vinculados en doble sentido y gestiona las actualizaciones, mientras en Riot debe hacerse con código y usando el métodothis.update()
.
Riot no requiere un método comosetup
pero requiere expresar las propiedades usandoprops.
en el template.
A continuación veremos un ejemplo en el que podemos hacer un ejercicio evitando compilar en tiempo de ejecución, es decir, incluyéndo los componentes listos para usar. Se combinarán tecnologías como Webpack
(empaquetador de archivos o recursos web, prerrequisito con algún conocimiento esencial).
Para empezar se debe crear una carpeta que corresponda al proyecto de ejemplo y en la que se instalarán los modulos requeridos de NodeJS
. Por tanto, allí se debe inicializar el arhivo package.json
ejecutando el comando npm init
e instalando los módulos de Webpack
y Riot
, así:
npm init
npm install riot @riotjs/webpack-loader @riotjs/compiler -D
npm install webpack webpack-cli webpack-dev-server -D
Dado que para nuestro ejercicio, Webpack
requiere un archivo de configuración, generalmente denominado webpack.config.js
, lo inicializamos (creándolo en un editor) con el siguiente contenido:
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, 'web', 'dist'),
filename: 'app.bundle.js'
},
module: {
rules: [
{
test: /\.riot$/,
exclude: /node_modules/,
use: [{
loader: '@riotjs/webpack-loader',
options: { hot: false }
}]
}
]
}
}
Modificamos el archivo de configuración package.json
generado por npm init
para incorporar el lanzador de inicio o tarea start
(bajo scripts
), así:
"scripts": {
"start": "webpack --mode development",
"serve": "webpack-dev-server --content-base web/ --mode development --open --inline --progress"
},
Bajo una carpeta que nombraremos web
creamos nuestro archivo index.html
con el siguiente contenido:
<!DOCTYPE html>
<html>
<head>
<title>Riot 5 - Hi there!</title>
</head>
<body>
<div id="app"></div>
<script src="dist/app.bundle.js"></script>
</body>
</html>
Además, abrimos una carpeta src
incluyendo el archivo del componente en Riot
, por ejemplo my-tag.riot
, con el siguiente contenido:
<my-tag>
<h3>{ props.title }</h3>
This is my first tag<br/>
<span class="counter" if={ state.timer > 5 }>{ state.timer }</span>
<style>
.counter {
font-size: 40px;
font-family: monospace;
font-weight: bold;
}
</style>
<script>
export default {
state: {
timer: 0
},
onMounted() {
setInterval(this.counter, 1000)
},
counter() {
this.update({ timer: ++this.state.timer })
}
}
</script>
</my-tag>
Se trata de un componente sencillo que visualiza un título recibido como parámetro (
props.title
) y después de 5 segundos visualiza un contador, el cual se inicia bajo el métodoonMounted
usando la funciónsetInterval
del lenguaje, que a su vez invoca acounter
para actualizar una propiedad (state.timer
). Aunque todo esRiot
se acerca aHTML
, se podría decir entonces que lo característico deRiot
en este ejemplo corresponde a:
{}
if
(condicional)onMounted
(que ejecuta código cuando monta el componente)this.update
(que actualiza el DOM)props
(para propiedades recibidas) y state
(para propiedades privadas)Javascript
(con export default
)También debemos crear bajo la carpeta src
un archivo app.js
que será usado como script
principal para armar el paquete con Webpack
. Colocamos el siguiente contenido:
import { component } from 'riot'
import Tag from './my-tag.riot'
component(Tag)(document.getElementById('app'), {
title: 'Hi there!'
})
Dado que hemos configurado webpack
para ser invocado con npm
, para generar nuestra versión distribuible ejecutamos:
npm start
Si bien, al tratarse de un ejercicio tan sencillo podríamos dirigirnos a la carpeta web
y abrir el archivo index.html
directamente desde el navegador, procedemos a iniciar un servicio web contando con webpack-dev-server
y como hemos asociado un “script” en el archivo package.json
, ejecutamos lo siguiente:
npm run serve
Una alternativa a webpack-dev-server
sería revisar si se tiene instalado un servidor web básico de NodeJS
como http-server
, incluso con Python usando en la línea de comandos python -m SimpleHTTPServer 8080
. Simplemente se inicia un servicio y para ver el resultado se consulta en el navegador con el puerto respectivo (generalmente 8080), ingresando en la dirección algo como localhost:8080
.
Al final tendremos un proyecto inicial con el siguiente inventario de archivos:
Archivo | Descripción |
---|---|
src/app.js | Archivo de script principal para armar el paquete |
src/my-tag.riot | Archivo del componente web o tag |
web/index.html | Pagina de arranque de la aplicación web o SPA |
web/dist/app.bundle.js | Archivo de script trasnpilado o distribuible (generado con webpack ) |
package.json | Archivo de configuración de paquetes o módulos NodeJS |
webpack.config.js | Archivo de configuración para distribución o empaquetado de la aplicación |
Basicamente se usan expresiones del lenguaje Javascript
delimitadas por signos de llaves ({}
) conforme al contexto, usando por ejemplo props
o state
, o una propiedad exportada.
{ state.value }
{ props.value || 'its string text' }
<p class={ this.selected: true }>
<button onclick={ hint }>
<script>
export default {
hint (e) { console.info('Ok!') }
}
</script>
Este último bloque de ejemplo en realidad corresponde a eventos que se manejan de modo semejante a cualquier expresión
Puedes manejar puntos de validación o condiciones para ocultar o mostrar algo usando if
con expresiones de Javascript
.
<div if={ state.ok }>...</div>
Si vas a procesar contenido que requiere una mayor dinámica, puedes usar each
cuando se trate arreglos o iteraciones, por ejemplo, para tablas o listas.
<ol>
<li each={ item in props.list }>{ item.name }</li>
</ol>
<ol>
<li each={ (row,i) in state.list }>{ i } - { row.name }</li>
</ol>
Este último ejemplo ilustra el uso con un arreglo imprimiendo también su índice
Si te preguntabas como puedes aplicar estilos personalizados en alguna parte del componente, simplemente continúa usando <style>
como en html
, así que si eres un programador con algún vacío sobre css
y cuentas con colaboración en equipo, un diseñador web te podría orientar.
<my-tag>
<h1>Title</h1>
<style>
h1 {
font-family: Arial;
}
</style>
</my-tag>
Quizás este sea uno de los puntos más claves a asimilar para un iniciado en esta librería. El ciclo de vida de un componente bajo Riot
corresponde al modo de interactuar internamente con el DOM
(este tipo de librerías facilitan un trabajo en el que se manipula el contenido) y las instancias por las que pasa un componente desde el momento en que se prepara para ser usado (onBeforeMount
) hasta el evento en que es removido (onUnmounted
), en caso de intervenir en su comportamiento de manera programada, es decir, es posible personalizar los eventos de ser necesario. Veamos entonces los 6 eventos a conocer.
Evento | Descripción |
---|---|
onBeforeMount | Antes de que el componente sea montado en la página |
onMounted | Justo después de que el componente es montado o renderizado por primera ocasión (o cada vez que se vuelve a montar) |
shouldUpdate | Punto de validación para proceder con una actualización visual o no conforme a lógica implementada. Debe retornar verdadero o falso y se evalua con los parámetros newProps para nuevos valores de propiedades recibidas, currentProps para las propiedades como se encuentran, incluso this para culquier otra propiedad interna, de modo que es posible resolver las condiciones necesarias justo antes de iniciar cualquier actualización dentro del componente. |
onBeforeUpdate | Justo antes de efectuar la actualización |
onUpdated | Justo después de que el componente es actualizado |
onBeforeUnmount | Antes de que el componente sea removido |
onUnmounted | Cuando el componente es removido |
export default {
onBeforeMount(props, state) {},
onMounted(props, state) {},
shouldUpdate(newProps, currentProps) {},
onBeforeUpdate(props, state) {},
onUpdated(props, state) {},
onBeforeUnmount(props, state) {},
onUnmounted(props, state) {}
}
Para refrescar el DOM o aplicar actualización se hace la siguiente invocación:
this.update()
this.update({ data: 'hi' })
<my-tag>
<slot />
</my-tag>
Lo anterior se hace en la definición del
TAG
, en el momento de usarse se hace lo siguiente:
<my-tag>
<span>Hi there!</span>
</my-tag>
Para multiples slots
primero se deben definir éstos con name
.
<my-tag>
<h1>
<slot name="one" />
</h1>
<div>
<slot name="two" />
</div>
</my-tag>
Al invocarse se utiliza como atributo slot
del modo siguiente:
<div>
<span slot="one">Welcome</span>
<span slot="two">John Doe</span>
</div>
Quizás parezca que es otra manera de hacer las cosas sin necesidad de usar propiedades, pero esto se debe a que los ejemplos se encuentran en un nivel esencial. Lo que se está sugiriendo es una anatomía para escenarios distintos en dónde se inyecta todo un bloque de
HTML
al interior de nuestro componente y este debe saber a qué parte corresponde.
Distinto al uso de HTML5
nativo, cuando dentro de un componente lo que se requiere es usar otro dentro de éste, el archivo debe importarse e indicarse el subcomponente dentro del elemento components
en la definición. Por ejemplo:
<my-tag>
<my-sub />
</my-tag>
<script>
import MySub from './my-sub.riot'
export default {
components: { MySub }
}
</script>
Nótese que se usa una convención de nombre para las etiquetas como
my-sub
y se importa con un nombre distinto dónde la primera letra de cada nombre está en mayúscula y se omite el guión, usando así en elscript
un nombre comoMySub
. El archivomy-sub.riot
deberá contener el código respectivo para el subcomponente.
Si te ha motivado usar Riot
para páginas web estáticas (en las que no interviene el lado del servidor) las cosas no paran allí. Es posible devolver un plantilla desde el servidor, o que se denomina renderizado desde el servidor (SSR). Para usar Riot
desde el servidor se requiere Node.js
, podrías revisar la salida con el siguiente ejemplo:
<my-tag>
<h3>Hi there!</h3>
{ props.title }
</my-tag>
El anterior contenido correspondería a un archivo para el componente o
tag
. Suponiendo que el anterior archivo se ha denominadomy-tag.riot
, se abriría un archivoJavascript
(por ejemplo:ssr.js
), como el siguiente:
const fs = require('fs');
const render = require('@riotjs/ssr');
const register = require('@riotjs/ssr/register');
register();
const name = 'my-tag';
const MyTag = require(`./${name}.riot`);
const tag = render.default(name, MyTag, { title: 'Hi there!' });
fs.writeFileSync(`./${name}.js`, MyTag, { encoding: 'utf8' });
const html = `<!DOCTYPE html>
<html>
<body>
${tag}
<script src="https://unpkg.com/riot@4/riot+compiler.min.js"></script>
<script type="module">
import MyTag from '/${name}.js';
riot.register('${name}', MyTag)
riot.mount('${name}')
</script>
</body>
</html>`;
console.log(html); // put: <my-tag></my-tag>
Esta librería tiene un tamaño pequeño y un buen tiempo de respuesta, de modo que uno se plantearía que tan necesario sería un renderizado desde el servidor, aún así si llega a ser necesario implementar esa dinámica la función respectiva tiene un objetivo esencial. En este caso obtiene
<my-tag></my-tag>
de modo dinámico desde el servidor.
Una alternativa es hacer uso del compilador (@riotjs/compiler
en lugar de @riotjs/ssr
). Veamos el nuevo escenario:
const fs = require('fs');
const riotC = require('@riotjs/compiler');
const name = 'my-tag';
const riotFile = `./${name}.riot`;
const jsFile = `./${name}.js`;
try {
const content = fs.readFileSync(riotFile, { encoding: 'utf8' });
const script = content.toString();
const { code, map } = riotC.compile(script);
fs.writeFileSync(jsFile, code, { encoding: 'utf8' });
const html = `<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/riot@4/riot+compiler.min.js"></script>
<script type="module">
import MyTag from '/${name}.js';
const createApp = window.riot.component(MyTag);
const app = createApp(document.getElementById('app'), {})
</script>
</body>
</html>`;
console.log(html);
}
catch (e) {
console.error(e.message);
}
Esto tendría cierta semejanza al diseño web con lenguajes que posibilitan un pre-procesamiento como PHP
, incluso JSP
o ASP
, en este caso la principal diferencia es contar con doble dinamismo al procesarse o compilarse desde el servidor e interactuar con las características de la librería una vez quede en las manos del navegador. Además, se percibe unificación al usar el mismo lenguaje Javascript
(tanto en el código para cliente como en el servidor).
Habiendo comprendido los fundamentos de esta librería, se plantea continuar con un proyecto nuevo que tendrá sus respectivas piezas de código. Se puede copiar el archivo de configuración webpack.config.js
del proyecto anterior y abriendo una nueva carpeta se puede inicializar el proyecto ejecutando los siguientes comandos (estando dentro de la carpeta):
echo {} > package.json
npm install riot @riotjs/webpack-loader @riotjs/compiler -D
npm install webpack webpack-cli -D
npm install serve-static restana -S
En el archivo package.json
se incorpora el elemento scripts
con lo siguiente:
"scripts": {
"start": "webpack --mode development",
"serve": "node server.js"
},
Como plantilla html
tendríamos lo siguiente:
<!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>
Se puede copiar el archivo app.js
del proyecto anterior como script
principal de nuestro código que se invoca desde el navegador (cliente). Nuestro archivo my-tag.riot
tendría el siguiente código:
<my-tag>
<div class={ state.color }>
<div class="ui text container">
<br/>
<form onsubmit={ onSend }>
<div class="ui labeled action fluid input">
<div class="ui label">http://localhost:3000/yourname/</div>
<input type="text" name="parameter" placeholder={ state.name }>
<button class="ui icon button">
<i class="search icon"></i>
</button>
</div>
</form>
<div class="ui toggle checkbox">
<input type="checkbox" onchange={ onCheck }>
<label><i class="adjust orange icon"></i></label>
</div>
<div class="ui placeholder segment" id="holder" if={ state.first }>
<div class="ui icon header">
<a onclick={ onSend }><i class="search blue link icon"></i></a>
Try it!
</div>
</div>
<pre>
<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: {
first: true,
name: 'john',
color: 'dark'
},
onMounted() { // When component is mounted or created
this.update()
},
onUpdated() { // When component is updated (after this.update)
if (!this.state.first) hljs.initHighlighting() // use highlight.js
},
onSend(e) {
e.preventDefault()
this.update({ first: false, code: '...' }) // update or render again HTML (DOM)
let input = document.querySelector('[name="parameter"]').value || this.state.name
let uri = 'http://localhost:3000/yourname/' + input
fetch(uri)
.then(res => res.json())
.then(data => {
this.update({ code: JSON.stringify(data, null, ' ') })
})
.catch(err => {
this.update({ code: `${uri} => ${err.message}` })
});
},
onCheck(e) {
if (this.state.color == 'clear')
this.state.color = 'dark'
else
this.state.color = 'clear'
this.update()
}
}
</script>
</my-tag>
Implementamos nuestro propio servidor web (usando restana
y serve-static
) nombrándolo server.js
con el siguiente código:
const files = require('serve-static');
const service = require('restana')();
const port = 3000;
names = (req, res, next) => {
if (req.params.name.length < 3)
return res.send({ status: 'error', message: 'invalid name' });
res.send({ yourname: req.params.name });
}
service.use(files('./web'));
service.get('/yourname/:name', names);
service.start(port).then((server) => {
console.log(`Service listening at ${port}`);
});
Tendríamos entonces la siguiente distribución de archivos:
Se invoca a webpack
con npm start
y se ejecuta el servicio (ej. node server.js
o npm run serve
) para revisar nuestra aplicación en un navegador moderno.
© 2019 by César Arcila