usadas en Som Energia, para Pythoner@s
David García Garzón
Poner al día de las tecnologías de Frontend que usamos en Som Energía al personal que ya programa en Python temas de Backend.
Se supone una base en programación.
Haré paralelismos con tecnologías Python.
Inevitable: El único lenguage incluido en todos los navegadores (en cada uno a su manera)
Sintaxis familiar pero engañosa, no se comporta igual que C++ o Java. Repasaremos algunas trampas.
Es necesario entender algunas construciones que usamos en Mithril.
Tecnologia de backend, sí, pero nos da un entorno de desarrollo (como el que tenemos en python).
Constructor: Prepara el paquete de ficheros que se bajará el navegador. Deja obsoletos a Grunt, Gulp, Requirejs, Bower, Browserify…
Framework para desarrollar aplicaciones de página única que se ejecutan en el navegador.
Alternativas: React, Vue, Angular…
Especificación de componentes gráficos. Originalmente para Android.
Define aspecto, comportamiento, variaciones… de botones, menus, radio buttons…
Hay muchas implementaciones (incompletas todas) Usamos la de Google para Web:
Material Design Components for the Web
Agnóstica al framework, la adaptamos a Mithril
Javascript, tan familiar que pensamos que no hay que aprenderlo.
Se parece a C++ y Java
Esperamos que se comporte como ellos.
Cuando no lo hace, 😠 😠 😠 😠 \(`O´)/
==
y !=
==
y !=
intentan convertir tipos antes de comparar===
y !==
comparan sin conversion1=='1' // true ''==' ' // false null == undefined // true
1==='1' // false false=='' // true null === undefined // false
0=='' // true false==0 // true undefined==false // false
0==='' // false [1,2]=='1,2' // true
La regla: Usar el de tres signos al menos que realmente quieras la conversión (y no, no la quieres)
+
En Python estamos muy acostumbrados a usarlas en condiciones y funcionan muy distinto.
{}
y []
son true
!!''
es false
, pero tambien uno con solo espacios: ' '
0
es falso pero, tambien el string '0'
!!false
: ' 000 \t\n '
Típico: me equivoco en el nombre de la variable
Javascript no se queja, devuelve undefined
.
Se propaga en silencio como false
o el numero NaN
o la cadena 'undefined'
… hasta que peta!!!
Añadir 'use strict';
al principio de los ficheros hace que se queje si usas una variable no declarada.
¡Ojo! Avisará con las variables, pero no con las propiedades de los objetos que no existen.
Los objetos de Javascript son “diccionarios”
Acceso dual con .
y []
como nuestro yamlns
.
var name = 'd';
var o1 = { // literal
a: 1,
'b-at': 2, // 'b-at' no es identificador valido, comillas
name: 19, // coge la clave 'name', no 'd'
name+'': 3, // truco para coger 'd' de la variable
};
// Updating: en Python: o1.update({'a':4, 'c':5})
Object.assign(o1, {a: 4, c: 5});
// Cloning: en Python: o2 = dict(o1)
var o2 = Object.assign({}, o1);
Métodos: atributos que apuntan a funciones
var MySingleton = {
_param1: 'param1value',
method1: function() {
// body
},
method2: outerfunction,
};
MySingleton.method3 = function() {};
No son clases, son Singletons, objetos únicos. Pero sirven de prototipo para otros.
En Mithril los usamos para definir componentes.
function MyClass(param1) { // funcion 'Factoria'
this._param1 = param1; // Attributo
function premethod1() {
// body
} // <- Sin punto y coma, es una declaracion
this.method1 = premethod1;
this.method2 = function() {
// body
}; // <- Ojo el punto y coma! es una asignacion
}
var myinstance = new MyClass('param1value'); // No olvides el new
// Ampliar una clase a posteriori. No olvides el 'prototype'!
MyClass.prototype.method3 = function() { ... };
this
Su valor en una funcion f
depende de como se llame, no de donde se defina
// La misma funcion todo el rato
function f (a,b) { console.debug(this); }
f(a,b); // Seria `undefined`
new f(a,b); // Seria un nuevo objeto `{}` vacio
o.f=f; o.f(a,b); // Seria `o`
var f2 = o.f; f2(a,b); // 'undefined', f2 pierde el binding
var f3 = f.bind(o); f3(a,b); // Seria `o`
// A bajo nivel
f.call(o, a, b); // Seria `o`
f.apply(o, [a,b]); // Seria `o`
this
Ojo con las lambdas y las inner:
Facilitan la programación asíncrona.
function funcionAsincrona() {
return new Promise(function (resolve, reject) {
// do you async stuff here
if (ok) { resolve(result); } // makes the promise succed
else { reject(error); } // makes the promise fail
});
}
var promesa = funcionAsincrona();
promesa.then(function(result) {
// Codigo a ejecutar cuando acabe
}).catch(function(error) {
// Codigo a ejecutar si falla
});
La definición del proyecto se guarda en package.json
.
Se crea con el comando npm init
a partir de preguntas interactivas.
npm
es un gestor de paquetes parecido a pip
npm search
, npm install
…
Se baja los paquetes de un repositorio online y los instala en local, en la carpeta node_modules
.
Cada proyecto tiene su node_modules
propio y aislado como un virtualenv
en Python.
Dependencias (de run-time): de uso del paquete
Dependencias de desarrollo: de construcción del paquete
Para nosotros, que no hacemos paquete (aun), las de run-time son las que se usan en el navegador.
Se definen en el package.json
y se instalan con npm install
sin especificar paquete.
Se añaden con npm install --save paquete
o con --save-dev
si es de desarrollo.
Con las opciones save quedan guardadas en las claves dependencies
y devDependencies
Si no, no se actualiza (los marca straneous)
Queda guardada la versión. Para actualizarlas: npm update --save paquete
Podemos añadir comandos personalizados para el desarrolo de nuestro proyecto.
El package.json
contiene la clave scripts
con comandos personalizados de npm
.
npm init # Crea el package.json a base de preguntas
npm install # Instal·la les dependencies del package.js
npm search <words> # busca paquetes con words en la descripción
npm install --save <package> # añade la dependencia al proyecto
npm install --save-dev package # añade dependencia de desarrollo
npm update --save/--save-dev # actualiza versión (menores) de los paquetes
npm run test # Ejecuta el script `test` definido en `package.json`
A partir del codigo fuente, genera los ficheros que se bajará el navegador.
Vamos que compila, ¿pero Javascript no era interpretado?
A medida que los proyectos se hacen grandes, es necesario modularizar, descartar modulos no usados, agregar el resto, optimizar, preprocesar…
Repositorio webforms-mithril
(link)
La configuración de webpack en webpack-config.js
Comandos de webpack
en los scripts
del package.json
A pelo, Javascript ES5 no sabe de modulos.
Se incluye cada .js en el html con <script>
.
Tambien las dependencias!
Todo va al scope global. No hay namespaces
.
Se usan funciones auto-llamadas para aislar.
Explora las dependencias entre los módulos y genera código para:
require
para cargarlosTambien modifica el html para incluir los assets finales
(function(modules) { // funcion auto-llamada
var installedModules = {}; // The module cache
function require(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
exports: {}
};
// Execute the module function
modules[moduleId].call(module, module.exports, require);
// Return the exports of the module
return module.exports;
}
require('./main'); // el entry point del bundle
}) ({
'./mymodule': function(module, exports, require) {
eval('string con el contenido del fichero');
},
'./main': function(module, exports, require) {
eval('string con el contenido del fichero');
},
});
Entry point: Punto de partida (js) de donde estirar las dependencias. Puede haber varios (diferentes páginas)
Los navegadores cargan más rápido un fichero mediano que muchos pequeños.
Bundle: Un fichero que junta las dependencias de un punto de partida.
var config = {
context: path.resolve(__dirname, 'src'), // our code's root
entry: {
main: './main', // bundle name -> src/main.js
},
output:
// Apends a chunkhash to force reloading
filename: 'bundle-[name]-[chunkhash].js',
},
plugins:[
// webpack deals with js. html by plugins
// Generaes html to insert generated css and js
new HtmlWebpackPlugin({
filename: 'index.html',
}),
Un fichero grande también es peor que varios medianos. Fragmentemos.
Los assets se generan con un hash en el nombre para forzar recarga de cache si hay cambios.
Las dependencias vendor son menos proclives a cambiar, si las separamos tendran mas hits de cache.
Con varios entry points, habrá cosas comunes entre los bundles. Separando lo común y lo particular, se optimiza la carga de múltiples páginas.
...
entry: {
main: './main',
contact: './contact', // added: second entry point
},
output:
filename: 'bundle-[name]-[chunkhash].js',
// added: id would be 'main~contract` for the shared one
chunkFilename: 'chunk-[id]-[chunkhash].js', // added
},
plugins:[
new HtmlWebpackPlugin({filename: 'index.html'}),
// Each page should have its html plugin
new HtmlWebpackPlugin({filename: 'contact.html'}),
Datos (json, xml, yaml), estilos (css, sass, less, stylus), imagenes (png, jpg, svg) …
Se requieren como si fueran javascripts.
Una cadena de loaders se encarga de que esten disponibles en el navegador.
Los loaders nos permiten traducir recursos en formatos no soportados por los navegadores.
También se usan para la optimización. Minifiers, uglifiers, imagenes multiresolución…
Los loaders determinan en que forma estará disponible el recurso y lo hacen accesible de forma transparente
Como ficheros independientes
Como strings en el bundle javascript
Extraidos en su propio bundle
Insertados en otro recurso
config.module.rules
como cargar cada recurso.
rules: [
// yaml-loader: yaml data -> json data
// json-loader: json data -> js object
// you get it with 'require'
{ test: /\.yaml$/, use: ["json-loader", "yaml-loader" ] },
// stylus-loader: stylus -> css
// css-loader: css -> js code that adds the style
// MiniCssExtractPlugin: extracts into a css bundle
// 'require' ensures that the css is loaded
// as any dependant assets (images, includes...)
{ test: /\.styl$/, use: [ MiniCssExtractPlugin.loader,
"css-loader", "stylus-loader"]},
// The css bundle is configured in the plugin section
Los loaders son solo tuberias.
Los plugins aportan loaders nuevos pero tambien otra funcionalidad.
HtmlWebPackPlugin
: Genera un html a partir de un template incluyendo uno o varios chunks (js, css…)
Problema: el HTML lo genera la aplicacion de backend (flask, django, php), no webpack.
Hay plugins de webpack que generan manifiestos con la lista de assets.
Hay extensiones para Flask y Django que integran esa lista en las páginas.
Estrategia: en backend pocas páginas y mucha API.
webpack-dev-server --open -d
o npm run server
Lanza un webserver NodeJS local con los assets
Apunta el navegador a la página principal.
Detecta cambios en los ficheros, regenera los assets del servidor y recarga el navegador.
Agiliza mucho el ciclo de desarrollo.
Con tanta transformación, ¿cómo relacionamos un error en el navegador con el código fuente?
En modo desarrollo webpack genera source maps. Comentarios especiales que referencian al código original, fichero y linia.
Los navegadores modernos los entienden y generan stack traces usables.
HTML y CSS permiten algunas animaciones y respuesta interactiva, pero son en esencia estàticos. JavaScript permite modificar el HTML en el navegador.
La librería estándar no es muy potente. Hay librerías que la complementan: JQuery, Underscore, Sugar…
No es suficiente abstracción para construir aplicaciones complejas. Ahí entran los frameworks: Vue, React, Angular, Mithril…
Los frameworks suelen dar una forma de definir componentes gráficos o widgets.
En HTML sería un tag propio, que controlamos vía atributos, y que abstrae un HTML más complejo.
Se pueden replicar y juntar con otros para armar nuevos componentes.
Cada framework llama vista, modelo o controlador a cosas distintas. El patron MVC original no era práctico y nadie lo usa tal cual. Pero quedó la idea: Separar la presentación de los datos.
El framework define:
Como el modelo (datos js) altera la vista (html)
Como lo que pasa en la vista modifica el modelo
m.xxxx
- acceso a funciones de la api Mithril.
m(...)
- como función genera nodos virtuales.
Los nodos virtuales (vnodes) representan HTML sin usar el DOM directamente, que es lento.
m.render
convierte nodos virtuales en HTML.
Renderiza solo una vez, nos vale para ejemplos. Veremos adelante como actualizar.
// Sintaxis general
m(tag, attrs, children, children2, ...);
// tag: sintaxis css
'h1' // <h1>
'.sidebar.black' // <div class="sidebar black">
'#mytag' // <div id="mytag">
'[title="tooltip"]' // <div title="tooltip">
// Combinable:
'input.mdc-input#name[type="text"]'
// attrs: (opcional) diccionario con más attributos
// children: (opcional, multiple) string, vnodes o lista childrens
var backuri = 'http://example.com';
return m('section#section1',[ // <section id="section1>
m('h2', 'Titulo'), // <h2>Titulo</h2>
m('p.first','yes'), // <p class="first">yes</p>
m('p','no'), // <p>no</p>
m('nav.backlink', [ // <nav class="backlink">
m('a[target="_blank"]', { // <a target="_blank"
'href': backuri, // href="http://examp..."
}, // >
'Volver atras' // Volver atras
), // </a>
]), // </nav>
]); // </section>
Las estructuras de control if
y for
rompen la estructura visual del hyperscript.
Mejor usar expresiones ternarias o cortocircuitos booleanos y, para los loops, map
.
Es un objeto/diccionario con:
tag
, attrs
y children
: los parámetros del m
text
: si solo hay un children y es textodom
: elemento DOM renderizado (si lo está)state
, key
: los veremosManipular el DOM dispara el redibujado. Los vnodes son baratos de crear y comparar. Vale la pena generarlos a menudo a cambio de juntar cambios en un solo redibujado.
// Nuestro primer componente!
var Hello = {
// render llama al método view pasandole el vnode original
// Retornamos el vnode que se renderizará en su lugar
view: function(vn) {
// podemos acceder a los attributos del vnode original
return m('h1', 'Hola '+vn.attrs.name||'mundo');
},
};
window.onload = function() {
var element = document.getElementById('mithril-target');
// Usamos el componente como tag
m.render(element, m(Hello, {name: 'Voki'}));
};
La funcion m.mount
activa el sistema de actualización. Después de cada evento se dispara un renderizado, si hay cambios se redibuja el DOM.
var App = {};
App.view = function(vn) { return m('h1', 'Hola mundo'); },
window.onload = function() {
var element = document.getElementById('mithril-target');
m.mount(element, App); // mount, no render!
};
Ojo! Al mount
le pasamos un componente, App
, no un vnode, m(App)
, como al render
.
Igual que usábamos vn.attrs
, podemos propagar vn.children
al virtual node resultante.
var Person = { name: 'anonymous' }; // the model
var PersonEditor = {};
PersonEditor.view = function(vn) {
return m('',[
m('input', {
value: Person.name, // Model -> View
oninput: function(ev) {
Person.name = ev.target.value; // View -> Model
},
}),
m('', 'hello ' + Person.name), // Model -> View (again)
]);
};
vn.state
: objeto que mantiene el estado del widget
PersonEditor.var1 = 1; // inicializa vn.state.var1
// Inicializa el estado en oninit
// Se llama antes de inicializar el DOM
PersonEditor.oninit = function(vn) {
vn.state.var2 = 'value2';
// vn.state se pasa como this a los metodos del componente
this.var3 = 'value3'; // igual que vn.state.var3
};
¡Ojo! Distingue entre estado interno y modelo
Hay que evitar manipular el DOM directamente.
A veces es necesario por el uso de otras librerías. (Como MDC4W).
vn.dom
: apunta al DOM renderizado
El componente puede implementar hooks que se llaman en diferentes momentos del ciclo de vida.
En vn.oninit()
no está disponible porque no se ha ejecutado aún ningún render.
oninit
: Antes de llamar al view
la primera vez. Para inicializar vn.state
a partir de vn.attrs
. Cosas que no necesiten el DOM.
oncreate
/onupdate
: Tras insertar/actualizar el DOM después de un render. Para llamar librerias que necesitan el DOM o consultar layout final.
¡Ojo! Cambios en el modelo aquí, no disparan render.
onremove
: Para tareas de limpieza. Se llama justo antes de eliminar el nodo.
onbeforeremove
: Para transiciones de salida. Retorna un Promise
. Se retrasa el onremove
y la eliminación del nodo hasta que el Promise
resuelva.
Es difícil saber que nodo virtual corresponde si reordenamos los nodos o si cambian demasiado.
Mithril permite asociar al virtual node una clave, vn.key
, para asegurar que el mapeo es correcto.
m.request({
url: 'https://example.com/api/persona',
method: 'POST',
data: {
name: Persona.name,
},
}).then(function(response) {
}, function(error) {
});
Se refresca la interfaz despues de recibir la respuesta.
Por defecto JSON, personalizable.
Diferentes niveles de personalizacion partiendo de:
extract(xhr, options)
: acceso a toda la respuesta, por defecto pasa xhr.responseText
deserialize(responseText)
: a partir del responseText, por defecto parser JSON
type(object)
: objeto JSON parseado, por defecto identidad
data
: datos que serializan en el body o en la request
headers
: añade cabeceras
config
: permite modificar las cabeceras de la request
serialize
: aplicado a data, por defecto JSON.serialize
Bibliotecas que definen elementos de la interfaz reusables (widgets)
Especificación de cómo han de ser las interfaces en Android a partir de Lollypop
Generalizado a otros soportes como el web.
Multiples implementaciones.
Button
Slider
Switch
TextField
Selects
Checkbox
RadioButton
LayoutGrid
List
GridList
ImageList
Tabs
Chip
Progress
AppBar
Drawer
Card
SnackBar
Banner
Dialog
Sheet
Lateral: pantallas al mismo nivel accesibles en todo momento. (Tabs, Drawer, Bottom Navigation Bar)
Progreso: profundizar en el nivel jerárquico (Button, List, Grid List, Image list…)
Atrás: Cronológico o jeràrquico (Back button)
Persistente
Bloqueante
Persistente
No bloqueante
Temporal
No bloqueante
Primary, Secondary y sus variantes light y dark.
Las variantes para destacar de forma harmoniosa.
Secondary para dar acento especial.
Background para el fondo estatico.
Surface para las cosas que se elevan sobre el fondo.
OnX: El color de texto cuando se usa X como fondo
Valores por defecto, customizables y criterios.
Fuente, tamaño, espaciado, mayúsculas…
Polythene: Lo usamos en el Tomàtic. Calcula los estilos en el navegador, y pierde lo que ganas con Mithril.
Mithril MDL: wrapper para Mithril de Material Design Lite de Google. Menos completo, mucho más rápido. Estilos precompilados.
MDL fue discontinuado en favor de Material Components 4 Web que aún no tiene wrapper Mithril.
Implementación Web de Google
Incompleta como todas, en progreso rápido.
En vez de concentrarse en un framework da herramientas para usarlo en cualquiera.
Nosotros haremos el wrapping para Mithril: src/mdc/
(link)
Estilos:
Implementados con Sass.
Customizables (¡precalculados!)
Javascript:
Cuando necesitan inicializacion
Cuando Ofrecen API.
En la raiz aplicar la clase mdc-typography
.
mdc-typography--headline1
a 6
mdc-typography--subtitles1
a 2
mdc-typography--body1
a 2
mdc-typography--caption
mdc-typography--overline
mdc-typography--button
Redefinibles en el CSS.
En nuestro css, antes de cargar el de MDC4W
* {
--mdc-theme-primary: red;
--mdc-theme-secondary: yellow;
--mdc-theme-background: white;
--mdc-theme-surface: #ffe;
/* cuando ponemos un color de tema de fondo,
estos colores para el texto */
--mdc-theme-on-primary: white;
--mdc-theme-on-secondary: black;
--mdc-theme-on-surface: black;
}
Usables como color var(--mdc-theme-primary, #faf)
Sin Javascript, basado solo en estilos
require('@material/button/dist/mdc.button.css');
var Button = {
view: function(vn) {
return m('button.mdc-button'
+(vn.attrs.raised ? '.mdc-button--raised':'')
+(vn.attrs.unelevated ? '.mdc-button--unelevated':'')
+(vn.attrs.outlined ? '.mdc-button--outlined':'')
+(+vn.attrs.dense ? '.mdc-button--dense':'')
, attrs, [
(vn.attrs.faicon ? m(
'i.mdc-button__icon.fa.fa-'+vn.attrs.faicon):''),
vn.children
]);
},
};
Inicializando y con API.
const mdcDialog = require('@material/dialog');
const MDCDialog = mdcDialog.MDCDialog;
var Dialog = {};
Dialog.oninit = function(vn) {
// Para poder acceder desde fuera a la API
vn.state.model = vn.attrs.model || {};
// Api publica del componente Mithril
vn.state.model.open = function() {
vn.state.widget.show();
};
};
Dialog.oncreate = function(vn) {
vn.state.widget = MDCDialog.attachTo(vn.dom);
vn.state.widget.listen('MDCDialog:accept', function() {
vn.attrs.onaccept && vn.attrs.onaccept();
});
vn.state.widget.listen('MDCDialog:cancel', function() {
vn.attrs.oncancel && vn.attrs.oncancel();
});
};
Dialog.onremove = function(vn) {
vn.state.widget.destroy();
};
Acceso al API via objeto injectado
const Dialog = require('./mdc/dialog');
var mydialog = {};
m(Dialog, {
oncancel: function() { }, // Whatever to do on cancel
onaccept: function() { }, // Whatever to do on accept
model: mydialog, // inject object
buttons: [
{ text: "Help", onclick: showhelp }, // Custom action
{ text: "No", cancel: true }, // Default cancel action
{ text: "Si", accept: true }, // Default accept action
},
}, m('¿Quieres proceder?)),
m(Buttton, {
// open is accessible via mydialog
onclick: function() { mydialog.open(); },
}, "Open Dialog");