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
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
lahdiouiouadie
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,125