WITH

Build a real-time PWA

AND

Ouadie LAHDIOUI

WHO ARE WE?

IT consultant @

CHIHAB OTMANI

Software Engineer | Trainer

Node.js Foundation Member

Co-Founder @

@lahdiouiouadie

@chihabotmani

Rabat.js

WHO ARE you ?

👨‍🎨 Front-End developers ?

👨‍💻 Back-End developers ?

🦸‍♂️ Something else ? something in between ?

☁️ Cloud developers ?

  • Architecture
  • Core components
  • Code samples
  • Goodies!

WHAT WE'RE GONNA SEE

Text

Architecture

Text

service 

REST API

SOAP API

db 

A PWA IS A WEB APP THAT USES MODERN WEB CAPABILITIES TO DELIVER AN
APP-LIKE USER EXPERIENCE.

WHY Not A NATIVE APP? 🤔

AVERAGE OF INSTALLED APP PER MONTH

0

NATIVE APPS

USER EXPERIENCE DEVELOPER EXPERIENCE
Installable Hard to multiplatform
Available Offline Ineffective workstyle
Engaging Store approval
Download required Expensive to develop
Updates required
No Google
No Multiplatform

HOW ABOUT A WEB APP? 🤔

WEB APPS

USER EXPERIENCE DEVELOPER EXPERIENCE
No install / updates Web technologies
Works on all devices Code Once Run Everywhere
Known by search engines Huge ecosystem
No offline Support 'Cheap' to develop
Not installable Secured over TLS/SSL layer
Not engaging

NATIVE APPS UX

PROGESSIVE WEB APPS

WEB APPS DX

PROGREsSIVE WEB APPS

USER EXPERIENCE DEVELOPER EXPERIENCE
Offline Support Web technologies
Engaging Code Once Run Everywhere
Installable Huge ecosystem
No install / updates 'Cheap' to develop
Works on all devices Secured over TLS/SSL layer
Known by search engines

With a home screen icon

With Offline Support

Receives notifications

A PWA is a Web app

Secured over HTTPS

HOME SCREEN ICON

WEB MANIFEST

WEB MANIFEST

{
  "short_name": "Twitter",
  "name": "Twitter App",
  "icons": [
    {
      "src": "/images/icons-192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "/images/icons-512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/?source=pwa",
  "background_color": "#3367D6",
  "display": "standalone",
  "scope": "/",
  "theme_color": "#3367D6"
}
<link rel="manifest" href="/manifest.json">

manifest.json

index.html

OFFLINE SUPPORT

AVAILABLE OFFLINE

Install/update application

Launch application while offline

SERVICE WORKER

A Service Worker is a script that the browser runs in the background.

It acts as a proxy between the application and the network.

AVAILABLE OFFLINE

No access to sync storage

Promise-based

Requires HTTPS

Service worker - REGISTRATION

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('./sw-test/sw.js')
  .then((reg) => {
    // registration worked
    console.log('Registration succeeded. Scope is ' + reg.scope);
  }).catch((error) => {
    // registration failed
    console.log('Registration failed with ' + error);
  });
}

main.js

Service worker - INSTALL

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/logo.jpg',
      ]);
    })
  );
});

Service worker - FETCH

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

Service worker - PUSH

self.addEventListener('push', function(event) {
  const title = 'PWA man!';
  const options = {
    body: 'Hey Devoxxians!',
    icon: 'images/devoxx.png'
  };
  event.waitUntil(self.registration.showNotification(title, options));
});

DO I HAVE TO DO ALL OF THAT?

@angular/PWA SCHEMATICS

ng add @angular/pwa
Installed packages for tooling via yarn.
CREATE ngsw-config.json (585 bytes)
CREATE src/manifest.webmanifest (1063 bytes)
CREATE src/assets/icons/icon-128x128.png (1253 bytes)
CREATE src/assets/icons/icon-144x144.png (1394 bytes)
CREATE src/assets/icons/icon-152x152.png (1427 bytes)
CREATE src/assets/icons/icon-192x192.png (1790 bytes)
CREATE src/assets/icons/icon-384x384.png (3557 bytes)
CREATE src/assets/icons/icon-512x512.png (5008 bytes)
CREATE src/assets/icons/icon-72x72.png (792 bytes)
CREATE src/assets/icons/icon-96x96.png (958 bytes)
UPDATE angular.json (3574 bytes)
UPDATE package.json (1350 bytes)
UPDATE src/app/app.module.ts (604 bytes)

SPA + ng add @angular/pwa = PWA

ANGULAR - WEB MANIFEST

{
  "name": "my-pwa",
  "short_name": "pwa",
  "theme_color": "#1976d2",
  "background_color": "#fafafa",
  "display": "standalone",
  "scope": "/",
  "start_url": "/",
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "assets/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    }
    ...
  ]
}

SERVICE WORKERS

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/logo.jpg',
      ]);
    })
  );
});

@angular/PWA SCHEMATIC

ng add @angular/pwa
Installed packages for tooling via yarn.
CREATE ngsw-config.json (585 bytes)
CREATE src/manifest.webmanifest (1063 bytes)
CREATE src/assets/icons/icon-128x128.png (1253 bytes)
CREATE src/assets/icons/icon-144x144.png (1394 bytes)
CREATE src/assets/icons/icon-152x152.png (1427 bytes)
CREATE src/assets/icons/icon-192x192.png (1790 bytes)
CREATE src/assets/icons/icon-384x384.png (3557 bytes)
CREATE src/assets/icons/icon-512x512.png (5008 bytes)
CREATE src/assets/icons/icon-72x72.png (792 bytes)
CREATE src/assets/icons/icon-96x96.png (958 bytes)
UPDATE angular.json (3574 bytes)
UPDATE package.json (1350 bytes)
UPDATE src/app/app.module.ts (604 bytes)

ANGULAR - SERVICE WORKER

{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }
    }
  ]
}

ngsw-config.json

"installMode": "prefetch",
"files": [
    "/favicon.ico",
    "/index.html",
    "/*.css",
    "/*.js"
]

ANGULAR - SERVICE WORKER

ng build --prod
{
  "configVersion": 1,
  "timestamp": 1572297590111,
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "updateMode": "prefetch",
      "urls": [
        "/index.html",
        "/main-es5.55c7be03cd19bb680a35.js",
        "/polyfills-es5.98f7268495936fc0a233.js",
        "/runtime-es5.d3647fbfa3de00cd0bdf.js",
        "/styles.3ff695c00d717f2d2a11.css"
        ...
      ]
    }
  ]
  ...
}

ngsw-config.json

ngsw-worker.js + ngsw.json

ANGULAR - SERVICE WORKER

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ServiceWorkerModule.register(
      'ngsw-worker.js', 
      { enabled: environment.production }
    )
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.module.ts

ANGULAR - SWUPDATE

@Injectable()
export class updateService {
  
  constructor(updates: SwUpdate, toastr: ToastrService) {
    
    updates.available.subscribe(event => {
      toastr.refresh('A new version is available');
    });
    
  }
  
}

ANGULAR - SWPUSH

constructor(private swPush: SwPush) {}  

subscribeToNotification() {
  this.swPush.requestSubscription({
    serverPublicKey: this.VAPID_PUBLIC_KEY
  })
  .then(sub => this.sendSubcriptionToBackend(sub))
}  

handleNotification() {
  this.swPush.notificationClicks.subscribe( event => {
    const dataFromBackend = event.notification.data;
    doSemthing(dataFromBackend);
  });
}

Progressive Web Apps ON PRODUCTION

ALIBABA

UBER

progressivewebapproom.com

AIRBNB

INSTAGRAM

TWITTER

HOW TO GET DATA? 🤔

recommendation 

APP become more complex 😨

invest

Sale

...

AI

Bot

IOT

...

service 

service 

service 

service 

🎉 DATA GRAPH LAYER  🎉

Bot

IOT

...

service 

service 

service 

service 

data GRAPH

APPs describe their requirement 🗣

services describe their capabilities 💪

🙅‍♂️ GraphQL is not a FramEwork

A query language for your API

And a server-side runtime for fulfilling those queries with your existing data

service 

REST API

SOAP API

service 

request

POST /graphQL

RESPONSE

{
  pikatchu: pokemon(id: "1") {
    name
    type
  }
}
{
  "data": {
    "pokemon": {
      "name": "Pikachu",
      "type": "Electric"
    }
  }
}

db 

GRAPHQL INTEREST OVER TIME

*source: Google search envolution

Graphql key concepts

*some of them

types & fields

TO DESCRIBE YOUR DATA

POKEMON {

}

NAME

TYPE

HEALTH

THIS IS A TYPE

these are fields

NAME

TYPE

HEALTH

Fields have functions (resolvers)

function pokemon_name(pokemon) {
  return pokemon.getName();
}
function pokemon_type(pokemon) {
  return pokemon.getType();
}
function pokemon_healthname(pokemon) {
  return pokemon.getHealth();
}

to get data frOm server

YOU SHOULD USE QUERIES

{
  pikatchu: pokemon(id: "1") {
    name
    type
  }
}
{
  "data": {
    "pokemon": {
      "name": "Pikachu",
      "type": "Electric"
    }
  }
}

GRAPHQL SERVER

REQUEST

RESPONSE

THIS IS an argument

THIS IS an alias

query BestPokemon {
  pikatchu: pokemon(id: "1") {
    name
    type
  }
}

To modify server-side data

YOU NEED TO USE MUTATIONS

mutation CreatePokemonMutation() {
  createPokemon(name: "Pikachu") {
	id
	type
  }
}
{
  "data": {
    "pokemon": {
      "id" : 1
      "name": "Pikachu",
      "type": "Electric"
    }
  }
}

GRAPHQL SERVER

REQUEST

RESPONSE

TO PUSH DATA FROM SERVER TO  CLIENTS

GRAPHQL SPEC SUPPORTS SUBSCRIPTIONS

Bot

IOT

service 

service 

service 

service 

data GRAPH

subscription onPokemonCreateSubscription(){
  onPokemonCreate(){
    id
    content
  }
}
subscription onPokemonCreateSubscription(){
  onPokemonCreate(){
    id
    content
  }
}
subscription onPokemonCreateSubscription(){
  onPokemonCreate(){
    id
    content
  }
}

Defining a Schema

type Query {
  allPokemons(last: Int): [Pokemon!]!
}
type Query { ... }
type Mutation { ... }
type Subscription { ... }
type Mutation {
  createPokemons(name: String!, type: String!): Pokemon!
}
type Subscription {
  newPokemon: Pokemon!
}

special root types

And a server-side runtime for fulfilling those queries with your existing data

GraphQL is served over HTTP

VIA A SINGLE ENDPOINT

who's the best 🤔

GRAPHQL vs OTHER API Styles

Both GRAPHQL vs REST vs SOAP “movements” are clearly fueled by unhappy, overlooked and over-served API consumers

colonial architecture 

CHIMNEY

gable roof

colonial houses

had colonial constraints

Listen to your constraints 

I really like colonial houses

I think i'll build one ... 😍

I really like colonial houses

REST APIS

I think i'll build one ... 😨

I really like colonial houses

GRAPHQL APIS

I think i'll build one ... 😰

TWO types of constraints 

API Product constraints

API Style constraints

+

APOLLO CLIENT

Apollo Client is a Feature-rich GraphQL client that manages data and state in an application.

APOLLO CACHE

Apollo Cache is a Reactive store for your GraphQL requests

APOLLO LINK

A sort of middleware for your GraphQL Requests

APOLLO TOOLS

APOLLO ANGULAR

APOLLO ANGULAR - Schematics

ng add apollo-graphql

CREATE src/app/graphql.module.ts (628 bytes)
UPDATE package.json (1728 bytes)
UPDATE tsconfig.json (572 bytes)
UPDATE src/app/app.module.ts (1702 bytes)

apollo ANGULAR - GRAPHQL MODULE

const uri = 'https://myapp.io/graphql';
export function createApollo(httpLink: HttpLink) {
  return {
    link: httpLink.create({uri}),
    cache: new InMemoryCache(),
  };
}

@NgModule({
  exports: [ApolloModule, HttpLinkModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink],
    },
  ],
})
export class GraphQLModule {}

apollo ANGULAR - QUERIES

const GET_USERS = gql`users { email firstName lastName }`;

constructor(private apollo: Apollo) { }
getUsers() {
  this.apollo
    .watchQuery({
      query: GET_USERS,
    })
    .valueChanges
    .subscribe(result => {
      this.data = result.data && result.data.users;
      this.loading = result.loading;
      this.error = result.error;
    });
}

apollo ANGULAR - MUTATIONS

const CREATE_USER = gql`mutation createUser(user: CreateUserInput!) {
    createUser(user: $user) {id email firstName lastName}
}`;

constructor(private apollo: Apollo) { }
createUser({ email, firstName, lastName }) {
  this.apollo.mutate({
    mutation: CREATE_USER,
    variables: {
      user: { email, firstName, lastName }
    }
  })
  .subscribe(
    user => this.user = user
  );
}

apollo ANGULAR - SUBSCRIPTIONS

const CREATE_USER = gql`subscription {onUpdateTopic { id name active }}`;

constructor(private apollo: Apollo) { }
subscribeToUpdate({ email, firstName, lastName }) {
  return this.apollo
   .subscribe({
     query: gql`${subscription}`,
   })
   .subscribe(
     topic => this.topic = topic
   );
}

AWS AppSync Client

AppSync Client is an Apollo Client

  • Offline Support
  • Authorization
  • Subscription Handshaking

AppSync Client - OFFLINE

AppSync Client - REAL-TIME

AppSync Client

export const environment = {
  production: true,
  aws: {
    aws_project_region: 'eu-west-1',
    aws_appsync_graphqlEndpoint: 'https://xxx.region.amazonaws.com/graphql',
    aws_appsync_apiKey: 'da2-xxx',
  }
};
npm install aws-appsync

AppSync Client

import { AUTH_TYPE, AWSAppSyncClient } from 'aws-appsync';
import { environment } from 'src/environments/environment';

@NgModule({
  exports: [ApolloModule, HttpLinkModule],
})
export class GraphQLModule {
  constructor(apollo: Apollo) {
    const client: any = new AWSAppSyncClient({
      url: environment.aws.aws_appsync_graphqlEndpoint,
      region: environment.aws.aws_project_region,
      auth: {
        type: AUTH_TYPE.API_KEY,
        apiKey: environment.aws.aws_appsync_apiKey
      }
    });
    apollo.setClient(client);
  }
}

bonus

useful GraphQL tools

Graphiql

$ npm install -g @aws-amplify/cli

$ amplify init

$ amplify add api
? Please select from one of the below mentioned services: GraphQL
..
? Do you have an annotated GraphQL schema? Yes
? Provide your schema file path: src/schema.graphql
GraphQL schema compiled successfully.

$ amplify push
√ Downloaded the schema
√ Generated GraphQL operations successfully and saved at src\graphql
√ Code generated successfully and saved in file src\app\API.service.ts

GraphQL endpoint: https://xxxx.appsync-api.us-east-1.amazonaws.com/graphql
GraphQL API KEY: da2-xxxxx

AWS AMPLIFY

const awsmobile = {
  "aws_project_region": "us-east-1",
  "aws_appsync_graphqlEndpoint": "https://xxxx.appsync-api.us-east-1.amazonaws.com/graphql",
  "aws_appsync_region": "us-east-1",
  "aws_appsync_authenticationType": "API_KEY",
  "aws_appsync_apiKey": "da2-xxxxxx",
  "aws_content_delivery_bucket": "ngx-pwa-201911081804-hostingbucket-dev",
  "aws_content_delivery_bucket_region": "us-east-1",
  "aws_content_delivery_url": "https://xxxxxx.cloudfront.net"
};


export default awsmobile;

Thank you

lahdioui​ouadie

CHIHABOTMANI

Build a real-time PWA with Angular and GraphQL

By Ouadie LAHDIOUI

Build a real-time PWA with Angular and GraphQL

Build a real-time PWA with Angular and GraphQL

  • 2,100