2016
Gonzalo Ruiz de Villa
@gruizdevilla
@gft_es
a.k.a. h2
(GET, POST, HEAD)
(PUT, DELETE, LINK, UNLINK)
(OPTIONS, HOST Header, Keep-Alive)
Minimizar, contatenar, gzip, sprites, vulcanizar, inline, ...
(Head Of Line Blocking)
un recurso cada vez
https://http2.golang.org/gophertiles?latency=30
Repetición de cabeceras
GET /index.html
User-Agent: Mozilla/5.0 ...
Cookie: __atuvc=4%7C49; ...
Referer: http://www.3pi ...
GET /script.js
User-Agent: Mozilla/5.0 ...
Cookie: __atuvc=4%7C49; ...
Referer: http://www.3pi ...
GET /styles.css
User-Agent: Mozilla/5.0 ...
Cookie: __atuvc=4%7C49; ...
Referer: http://www.3pi ...
Compresión de cabeceras para http
que puedes tener en cuenta al construir una aplicación
Y otros intentos:
Tradeoff:
- 25 apps usadas mensualmente por un usuario
- 100+ sitios webs visitados por un usuario medio con Chrome sobre Android
In a consumer mobile app, every step you make a user perform before they get value out of your app will cost you 20% of users.
http://blog.gaborcselle.com/2012/10/every-step-costs-you-20-of-users.html
¿que es lo que echamos de menos? Fundamentalmente:
¿Es lo mismo que un bookmark?
example.com/
vs
example.com/date/title
Manifest start_url debe funcionar siempre
El sitio debe tener un origen seguro
Dos visitas de 5+ minutos
Con las propiedades adecuadas
Son parte de la web real
Forman parte de una iniciativa de estandarización
Y son muy adecuadas para móviles económicos, típicos de mercados emergentes
Web Manifest
Incluye metadatos para una Progressive Web App, como:
<link rel="manifest" href="/manifest.json">
@media (display-mode: standalone) {
h3:after {content: "¡En modo standalone!"}
}
{
"name": "Progressive Web App Demo",
"short_name": "Demo",
...
"start_url": "/simple-demo/?home=true"
}
Manifest generator
Manifest validator
Refresh, atrás y adelante
Compartir la UI, copiando un enlace
onbeforeinstall Event
Deep linking
Los Service Workers permiten eliminar la red de la ecuación
(en visitas posteriores)
1s de retraso de la carga de una pagina
11% páginas vistas menos
16% de reducción en satisfacción del cliente
http://www.radware.com/Products/FastView/
Proporcionar una experiencia de carga instantánea y consistente
¿Qué es la App Shell?
HTML: /shell
+
JavaScript: /app.js
+
Estilos: /styles.css
¿Qué es un Service Worker?
Una buena analogía es la de un controlador aéreo
Web page
Service Workers
Network
Cache
Ciclo de vida de un Service Worker
Evento install
<head>
<style>
/* estilos en linea */
</style>
</head>
<body>
<!-- punto de inserción de la plantilla -->
<script src="app.js">
</body>
/shell
Ciclo de vida de un Service Worker
Evento "install"
Evento "activate" (limpieza de cache)
Espera
Evento "fetch"
Volver a 4
Stop
Dos librerías para facilitar la vida
https://developers.google.com/web/showcase/case-study/service-workers-iowa
Dos librerías para facilitar la vida
Alcance y engagement
Service Worker
Web server
Service Worker
Web Server
Push Service
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
serviceWorkerRegistration.pushManager.subscribe()
.then(function(subscription) {
// The subscription was successful
return sendSubscriptionToServer(subscription);
})
.catch(function(e) {
//Permission denied or an error occurred
});
});
Service Worker
Web Server
Push Service
self.addEventListener('push', function(event) {
event.waitUntil(
fetch('/notification.json').then(function(response) {
return response.json();
}).then(function(data) {
return self.registration.showNotification(
data.title,
{
body: data.body,
icon: data.iconUrl,
tag: data.tag
}
);
});
);
});
Importante
Urgente
"Usa mi aplicación"
Comienza una emisión online
Ha llegado una domiciliación
Los valores caen por debajo de un límite
Foco en ventanas existentes
clients.matchAll({
type: 'window'
})
.then(function(clients) {
if (clients.length > 0) {
clients[0].postMessate({naviegateTo:url})
} else {
return clients.openWindow(url);
}
});
Device
Service
Characteristic
navigator.bluetooth.requestDevice({
filters: [{
services:['battery_service']
}]
})
navigator.bluetooth.requestDevice({
filters: [{
services:['battery_service']
}]
})
.then(device => {
log('Connection to ' + device.name);
return devie.connectGATT();
})
.then(gattServer => gattServer.getPrimaryService('battery_service'))
.then(service => service.getCharacteristic('battery_level'))
.then(characteristic => characteristic.readValue())
.then(buffer => {
let batteryLevel = buffer.getUint8(0);
log('> Battery Level is' + batteryLevel + '%');
})
characteristic.startNotifications().then(() => {
characteristic.addEventListener(
'characteristicvaluechanged',
handleNotifications
);
});
function handleNotifications(event) {
let buffer = event.target.value;
//...
}
Notificaciones
bluetoothDevice.request()
.then(() => bodySensorLocation.read());
Polymer element:
platinum-bluetooth
<platinum-bluetooth-device services-filter='["battery_service"]'>
<platinum-bluetooth-characteristic
service='battery_service'
characteristic='battery_level'>
</platinum-bluetooth-characteristic>
</platinum-bluetooth-device>
<span>{{bodySensorLocation}}</span>
bluetoothDevice.request()
.then(() => bodySensorLocation.read());
Polymer element:
platinum-bluetooth
<platinum-bluetooth-device services-filter='["battery_service"]'>
<platinum-bluetooth-characteristic
service='battery_service'
characteristic='battery_level'>
</platinum-bluetooth-characteristic>
</platinum-bluetooth-device>
<span>{{bodySensorLocation}}</span>
BLE Peripheral Simulator
1. Descubrir
2. Recuperar y ordenar
https://paradas.bus/p123
https://paradas.bus/p123
https://paradas.bus/p123
https://paradas.bus/p123
El puente entre la web y los dispositivos físicos
Es solo una web
Una web por dispositivo
Sin interrupciones
¿Son códigos QR?
¿SPAM?
Eddystone
UID
TLM
URL
Escenarios
<my-tabstrip>
<my-tab>
Home
</my-tab>
<my-tab>
Services
</my-tab>
<my-tab>
Contact Us
</my-tab>
<my-tabstrip>
Una librería "opinionated" para construir nuevos webcomponents
<link rel="import"
href="bower_components/polymer/polymer.html">
<script>
// register a new element called proto-element
Polymer({
is: "proto-element",
// add a callback to the element's prototype
ready: function() {
this.textContent = "I'm a proto-element. Check out my prototype!"
}
});
</script>
<!DOCTYPE html>
<html>
<head>
<script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="proto-element.html">
</head>
<body>
<proto-element></proto-element>
</body>
</html>
<link rel="import"
href="bower_components/polymer/polymer.html">
<dom-module id="dom-element">
<template>
<p>I'm a DOM element. This is my local DOM!</p>
</template>
<script>
Polymer({
is: "dom-element"
});
</script>
</dom-module>
<link rel="import"
href="bower_components/polymer/polymer.html">
<dom-module id="picture-frame">
<template>
<!-- scoped CSS for this element -->
<style>
div {
display: inline-block;
background-color: #ccc;
border-radius: 8px;
padding: 4px;
}
</style>
<div>
<!-- any children are rendered here -->
<content></content>
</div>
</template>
<script>
Polymer({
is: "picture-frame",
});
</script>
</dom-module>
<!DOCTYPE html>
<html>
<head>
<script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="picture-frame.html">
</head>
<body>
<picture-frame>
<img src="images/p-logo.svg">
</picture-frame>
</body>
</html>
<link rel="import"
href="bower_components/polymer/polymer.html">
<dom-module id="name-tag">
<template>
<!-- bind to the "owner" property -->
This is <b>{{owner}}</b>'s name-tag element.
</template>
<script>
Polymer({
is: "name-tag",
ready: function() {
// set this element's owner property
this.owner = "Daniel";
}
});
</script>
</dom-module>
<link rel="import"
href="bower_components/polymer/polymer.html">
<!-- import the iron-input custom element -->
<link rel="import"
href="bower_components/iron-input/iron-input.html">
<dom-module id="editable-name-tag">
<template>
<p>
This is a <strong>{{owner}}</strong>'s editable-name-tag.
</p>
<!-- iron-input exposes a two-way bindable input value -->
<input is="iron-input" bind-value="{{owner}}"
placeholder="Your name here...">
</template>
<script>
Polymer({
is: "editable-name-tag",
properties: {
owner: {
type: String,
value: "Daniel"
}
}
});
</script>
</dom-module>
Web
Mobile Web
Android
iOS
Windows
Mac
Linux
import {Component, bootstrap} from 'angular2/angular2';
import {TodoList} from './todoList.ts';
@Component({
selector: 'app',
directives: [TodoList],
template: `
<h1>Todo App</h1>
<todo-list></todo-list>
`
})
class AppComponent {
constructor() {
}
}
bootstrap(AppComponent);
import {Component, CORE_DIRECTIVES} from 'angular2/angular2';
import {TodoItem} from './todoItem.ts';
@Component({
selector: 'todo-list',
template: `
<ul>
<todo-item
*ng-for="#todo of todos"
[todo]="todo"></todo-item>
</ul>
`,
directives: [CORE_DIRECTIVES, TodoItem]
})
export class TodoList {
public todos: Array<any>;
constructor() {
this.todos = TODOS;
}
}
var TODOS = [
{
content: 'Discuss new feature',
isCompleted: false
},
{
content: 'Fix issue',
isCompleted: false
}
]
import {Component} from 'angular2/angular2';
@Component({
selector: 'todo-item',
properties: ['todo'],
template: `
<li class="todo-item">
{{todo.content}}
</li>
`
})
export class TodoItem {
}
export class TodoService {
private _todos;
constructor() {
this._todos = todos;
}
getTodos() {
return this._todos;
}
completed(todo) {
todo.isCompleted = todo.isCompleted ? false : true;
}
delete(todo) {
var index = this._todos.indexOf(todo);
this._todos.splice(index, 1);
}
add(content: string) {
var todo = {
content: content,
createdAt: new Date(),
isCompleted: false
}
this._todos.push(todo);
}
}
var todos = [
{
content: 'Write Angular 2 Blog',
createdAt: new Date(),
isCompleted: false
},...
]
...
import {TodoService} from './todoService.ts';
...
export class TodoList {
constructor(private todoService: TodoService) {
}
}
import {Component, CORE_DIRECTIVES} from 'angular2/angular2';
import {TodoService} from './todoService.ts';
@Component({
selector: 'todo-item',
properties: ['todo'],
directives: [CORE_DIRECTIVES],
styles: [
`
.todo-item.completed{
text-decoration: line-through;
}
`
],
template: `
<li [ng-class]="getTodoItemClass(todo)">
{{todo.content}}
-
<a (click)="todoService.completed(todo)">Completed</a>
-
<a (click)="todoService.delete(todo)">Delete</a>
</li>
`
})
export class TodoItem {
constructor(private todoService: TodoService) {
}
getTodoItemClass(todo) {
return {
completed: todo.isCompleted === true,
"todo-item": true
}
}
}
@Component({
selector: 'modal',
...
template: `
<div>
<h1 #inicio tabindex="-1">Título modal</h1>
...
<div (focus)="inicio.focus()" tabindex="0"></div>
</div>
`
})
export class Modal {
//...
}
Ventana modal accesible con control de foco de teclado
Al percibir la naturaleza tangible de un objeto ayuda a entender como manipularlo. El movimiento de un objeto transmite esta naturaleza: es ligero o pesado, flexible o rígido, grande o pequeño.
El movimiento no es solo algo bonito, proporciona información de relaciones espaciales, funcionalidad e intencionalidad del sistema.
Cada vez menos
Delay | User reaction |
---|---|
0-100ms | Instantáneo |
100-300ms | Retraso vagamente perceptible |
300ms-1s | Retraso perceptible, mantiene foco |
1s+ | Cambio de contexto mental |
10s+ | Ya vol |
60 fps
Reponse
Animation
Idle
Load
100ms percepción de respuesta instantánea
16 ms para realizar el trabajo antes de cada frame
8~10 ms en la práctica
50ms para responder a la interacción de un usuario
1s para que el usuario no se despiste
Response | Animation | Idle | Load |
---|---|---|---|
Tap to paint menos de 100ms |
Cada frame se pinta en menos de 16ms | Usar el tiempo ocioso para programar trabajo de forma proactiva | Satisfacer los objetivos de "Response" en la carga |
Drag to paint en menos de 16ms | Cada bloque de trabajo en 50ms | Conseguir pintar algo con sentido en menos de 1000ms |
RAIL pone al usuario en el centro