KEMBAR78
Building Progressive Web Apps for Windows devices | PPTX
A PWA is
Progressive
Works on any device and enhance
functionality progressively.
Discoverable
Better discovery and integration
with search.
Linkable
Ability to retain or reload its state
and be shareable from a link.
Responsive
Fit any device’s form factor and
screen size.
App-like
Looks like a native app and uses the
application shell model with minimal
page refreshes.
Connectivity-agnostic
Works with low connectivity or offline.
Installable
Install on the device’s desktop,
start menu, or task bar making it
readily available.
Current
Application and content is up to date
when connected to the Internet.
Re-engageable
Promotes re-engagement through
features such as push notifications.
Performant
Works as fast or faster than a native app.
Connected to users
Direct feedback to you through
ratings and reviews
Why PWA?
Our Goal:
The very best experience
for your Web content
will be on Windows
Keep your workflow for web
Build Deploy
Publish
App
Web
Content
Build
Build with web technology and web developers.
No need to find and hire c# developers.
Deploy
The work flow for web and PWAs are shared,
so your deploy process doesn’t need to
change to accommodate PWA.
Publish App
Your PWA “app container” needs only to be published once.
The content is always up to date because it comes from the web.
Web
The same app runs across browsers
and can take advantage of PWA
features as needed / supported.
PWA
The same app serve as a store app
with even more features and
additional reach to store users.
Devices + IoT Mobile PC Xbox Surface Hub HoloLens
A C R O S S W I N D O W S
C R O S S P L AT F O R M
PROGRESSI VE WEB APPS
X
HTTPS Web App Manifest Service Worker
Minimum Viable
Progressive Web App
Web Application
Manifest
{
"lang": "en",
"short_name": "Wash Post",
"name": "The Washington Post",
"icons": [
{
"src": "img/launcher-icon-2x.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "img/launcher-icon-4x.png",
"sizes": ”512x512",
"type": "image/png"
}
],
"start_url": "/pwa/?utm_source=homescreen",
"display": "standalone",
"orientation": "portrait",
"background_color": "black"
}
Web Application
Manifest
{
"lang": "en",
"short_name": "Wash Post",
"name": "The Washington Post",
"icons": [
{
"src": "img/launcher-icon-2x.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "img/launcher-icon-4x.png",
"sizes": ”512x512",
"type": "image/png"
}
],
"start_url": "/pwa/?utm_source=homescreen",
"display": "standalone",
"orientation": "portrait",
"background_color": "black"
}
Web Application
Manifest
{
"lang": "en",
"short_name": "Wash Post",
"name": "The Washington Post",
"icons": [
{
"src": "img/launcher-icon-2x.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "img/launcher-icon-4x.png",
"sizes": ”512x512",
"type": "image/png"
}
],
"start_url": "/pwa/?utm_source=homescreen",
"display": "standalone",
"orientation": "portrait",
"background_color": "black"
}
Web Application
Manifest
{
"lang": "en",
"short_name": "Wash Post",
"name": "The Washington Post",
"icons": [
{
"src": "img/launcher-icon-2x.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "img/launcher-icon-4x.png",
"sizes": ”512x512",
"type": "image/png"
}
],
"start_url": "/pwa/?utm_source=homescreen",
"display": "standalone",
"orientation": "portrait",
"background_color": "black"
}
App display Property
• Runs in Edge as a
separate windows or Tab.
• Full browser UI,
no store listing
Browser
• Runs in App container
in separate process.
• No browser UI,
back button only.
• Microsoft store listing
Standalone UI
• Runs in App container in
separate process.
• Limited UI (navigation
& read only nav bar).
• Microsoft store listing
Minimal UI
Network request:
Typical
Web App
Network
Network request:
Service Worker
in play
Web App
Service
Worker
Network Cache
if ( navigator.serviceWorker ) {
if ( navigator.serviceWorker.controller ) {
console.log('There’s an active service worker');
} else {
navigator.serviceWorker.register('/sw.js')
.then(function(reg) {
console.log('SW registered for scope',
reg.scope);
});
}
}
if ( navigator.serviceWorker ) {
if ( navigator.serviceWorker.controller ) {
console.log('There’s an active service worker');
} else {
navigator.serviceWorker.register('/sw.js')
.then(function(reg) {
console.log('SW registered for scope',
reg.scope);
});
}
}
This tests to see if Service Workers are supported.
if ( navigator.serviceWorker ) {
if ( navigator.serviceWorker.controller ) {
console.log('There’s an active service worker');
} else {
navigator.serviceWorker.register('/sw.js')
.then(function(reg) {
console.log('SW registered for scope',
reg.scope);
});
}
}
We can also see if there’s a Service Worker already running.
if ( navigator.serviceWorker ) {
if ( navigator.serviceWorker.controller ) {
console.log('There’s an active service worker');
} else {
navigator.serviceWorker.register('/sw.js')
.then(function(reg) {
console.log('SW registered for scope',
reg.scope);
});
}
}
If there isn’t, we can register one.
if ( navigator.serviceWorker ) {
if ( navigator.serviceWorker.controller ) {
console.log('There’s an active service worker');
} else {
navigator.serviceWorker.register('/sw.js')
.then(function(reg) {
console.log('SW registered for scope',
reg.scope);
});
}
}
By default, a Service Worker is scoped to its path.
addEventListener('fetch', function(evt) {
evt.respondWith(
fromCache(evt.request)
.catch(fromServer(evt.request)));
evt.waitUntil(update(evt.request));
});
addEventListener('fetch', function(evt) {
evt.respondWith(
fromCache(evt.request)
.catch(fromServer(evt.request)));
evt.waitUntil(update(evt.request));
});
In a Service Worker we can intercept any network requests (“fetch”).
addEventListener('fetch', function(evt) {
evt.respondWith(
fromCache(evt.request)
.catch(fromServer(evt.request)));
evt.waitUntil(update(evt.request));
});
And we can control the response. In this case responding from the cache first.
addEventListener('fetch', function(evt) {
evt.respondWith(
fromCache(evt.request)
.catch(fromServer(evt.request)));
evt.waitUntil(update(evt.request));
});
If no cached response is found, we can go to the network.
addEventListener('fetch', function(evt) {
evt.respondWith(
fromCache(evt.request)
.catch(fromServer(evt.request)));
evt.waitUntil(update(evt.request));
});
We can also update the cache for the next time the request is made.
 Validate your PWA is ready for Microsoft Store
 Improve perf and User Experience
https://sonarwhal.com/scanner/e2da8548-
6afe-4944-a736-dd3c286e2a87
dev.microsoft.com
reportapp@microsoft.com
PWABuilder.com
Value Add
of Windows Store App
// Am I installed in Windows?
if ( window.Windows ){
// pin a secondary tile
tile = new Windows.UI.StartScreen.SecondaryTile(tileId, text, text,
activationArguments,newTileDesiredSize, logoUri);
//place activation
var buttonCoordinates = { x: SR.left, y: SR.top, width: SR.width, height:
SR.height };
var placement = Windows.UI.Popups.Placement.above;
}
Access OS-level APIs
Secondary Pinning
PWA Activation Object
Windows.UI.WebUI.WebUIApplication.addEventListener('activated', function(args) {
if (args.kind === Windows.ApplicationModel.Activation.ActivationKind.voiceCommand) {
var results = args.result, spoken = results.text, command = results.rulePath[0], details,
redirect;
if (command === 'open') {
details = results.semanticInterpretation.properties.section[0];
switch (details) {
case "watchlist":
redirect = "https://www.mydomain.com/user/watchlist";
break;
case “login":
redirect = "https://www.mydomain.com/user/login";
break;
…
}
}
1
2
3
Getting Started
Configuration APIs
Feature APIs
Agenda
46
Section
Creating the Windows App
PWABuilder.com
Changed logos
Edited version info
Uploaded + Submitted as Beta
Integration testing
VirtualBox + VM, Charles Proxy to https://localhost.twitter.com
(via hosts file change, helps with HTTPS + CSP)
Dev Tools Preview Edition
Manually install/sideload to debug
47
Section
The Hard Parts
-API is massive!
-Lack / inaccuracy of JS documentation and examples
- Learn how to translate from C# (Class.Prop -> Class.prop)
- Hard to figure out the effect of certain calls from docs (e.g. review request)
-Boilerplate/config often imperative instead of config based
-ASync is not native JS Promises--cannot Promise.all()
- Can however just .then everything
48
Section
Configuration APIs / Core Path
Things that interact with Windows by setting or getting variables
May want them in the core render path, e.g. not code-split
Usually not async => no promises!
DO THIS FIRST
// This must not be deferred so that it can receive the initial 'activated' event in time
window.Windows.UI.WebUI.WebUIApplication.addEventListener(
'activated',
e =>
microsoftInterfaceLoader().then(microsoftInterface => {
microsoftInterface.handleActivatedEvent(e);
}),
false
);
// Without this, the app will first refresh to the start path before every activate event
window.MSApp.pageHandlesAllApplicationActivations(true);
50
Read / Set Up Window
51
Read / Set Up Window
52
Read / Set Up Window
const currentView = window.Windows.UI.ViewManagement.ApplicationView.getForCurrentView();
// Sometimes we were getting stuck in fullscreen mode because
// the user had closed the app with a video open
currentView.isFullScreenMode && currentView.exitFullScreenMode();
// 325px is a safe minimum size that allows the tweet layout to look OK
currentView.setPreferredMinSize({ width: 325, height: 400 });
53
Read / Set Up Window
const isWindowsDarkMode = () => {
const uiSettings = new window.Windows.UI.ViewManagement.UISettings();
const color = uiSettings.getColorValue(
window.Windows.UI.ViewManagement.UIColorType.background
);
return color.r === 0 && color.g === 0 && color.b === 0;
};
// Prefer cookie setting, fallback to WinPWA if not set
const cookies = canUseDOM ? cookie.parse(document.cookie) : {};
const cookieNightModeOn = cookies[NIGHT_MODE_COOKIE] === '1';
const windowsNightModeOn = isWindowsPwa &&
cookies[NIGHT_MODE_COOKIE] === undefined &&
isWindowsDarkMode();
(cookieNightModeOn || windowsNightModeOn) && StyleSheet.setTheme('dark');
Read / Set Up Window
55
Section
Feature APIs
Twitter Windows APIs
PasswordVault
Share Integration**
Notifications
Pinning
Jump Links **(for images)
Store reviews / Feedback
Timeline
Protocol Handling **
56
Section
Feature APIs
// Set a default interface so elsewhere in our code we don’t have to care
// what happens, we can just call this.context.hostInterface.visitedMomet
let hostInterface = {
onPostLoad: noop,
pinnedUser: noop,
visitedMoment: noop,
}
// If we are on Windows...load the interface for that host system...
// Now some functions may be defined and actually do things!
isWindowsPWA && microsoftInterfaceLoader().then(microsoftInterface => {
hostInterface = {
...hostInterface,
microsoftInterface
}
});
Code Split
Share!
window.navigator.share = ({ text, title, url }): Promise => ???
let shareData = {};
// On the windows api event, we then reattach it and format it
const handleShareEvent = e => {
const { text, title, url } = shareData;
const request = e.request;
request.data.properties.title = title;
request.data.properties.description = text;
const uri = new window.Windows.Foundation.Uri(url);
request.data.setWebLink(uri);
};
ShareShim.js
...
microsoftInterface.onPostLoad = () => {
if (window.navigator.share) { return; } // It’s a Polyfill
const dataTransferManager =
Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView();
dataTransferManager.addEventListener('datarequested', handleShareEvent);
// Windows interface means we trigger an event and then immediately consume it
// store the data here, for the event handler above to use
window.navigator.share = currentShareData => {
shareData = currentShareData;
window.Windows.ApplicationModel.DataTransfer.DataTransferManager.showShareUI();
return Promise.resolve();
};
};
ShareShim.js
Calling C# from a webview
namespace CommunicationWinRT
{
[Windows.Foundation.Metadata.AllowForWeb]
public sealed class CommunicationWinRT
{
public CommunicationWinRT()
{
}
public String GetValue()
{
return "From my WinRT component Yo!";
}
}
}
Calling C# from a webview
document.getElementById('toast').addEventListener('click', function () {
if (window.CommunicationWinRT) {
testToast.toastMessage(document.getElementById('input').value, 0)
} else {
alert(document.getElementById('input').value)
}
})
if (window.CommunicationWinRT) {
var testToast = new CommunicationWinRT.CommunicationWinRT();
}
Capabilities
UserExperience
Service Worker calling WinRT APIs
addEventListener('fetch', function(evt) {
evt.respondWith(
fromCache(evt.request)
.catch(fromServer(evt.request)));
evt.waitUntil(function(evt.request){
Windows.Storage.AccessCache.StorageApplicationPermissions
.futureAccessList.add(evt.request);
});
});
Remember
1. PWAs are part of Windows to enable
Web developers to build great user experiences.
2. PWAs can naturally or specifically
be added to the Microsoft store.
3. PWAs are powerful, flexible
and extensible on Windows.
4. We’re not done yet!
Building Progressive Web Apps for Windows devices
Building Progressive Web Apps for Windows devices
Building Progressive Web Apps for Windows devices
Building Progressive Web Apps for Windows devices

Building Progressive Web Apps for Windows devices

  • 3.
    A PWA is Progressive Workson any device and enhance functionality progressively. Discoverable Better discovery and integration with search. Linkable Ability to retain or reload its state and be shareable from a link. Responsive Fit any device’s form factor and screen size. App-like Looks like a native app and uses the application shell model with minimal page refreshes. Connectivity-agnostic Works with low connectivity or offline. Installable Install on the device’s desktop, start menu, or task bar making it readily available. Current Application and content is up to date when connected to the Internet. Re-engageable Promotes re-engagement through features such as push notifications. Performant Works as fast or faster than a native app. Connected to users Direct feedback to you through ratings and reviews
  • 4.
    Why PWA? Our Goal: Thevery best experience for your Web content will be on Windows
  • 5.
    Keep your workflowfor web Build Deploy Publish App Web Content Build Build with web technology and web developers. No need to find and hire c# developers. Deploy The work flow for web and PWAs are shared, so your deploy process doesn’t need to change to accommodate PWA. Publish App Your PWA “app container” needs only to be published once. The content is always up to date because it comes from the web. Web The same app runs across browsers and can take advantage of PWA features as needed / supported. PWA The same app serve as a store app with even more features and additional reach to store users.
  • 7.
    Devices + IoTMobile PC Xbox Surface Hub HoloLens A C R O S S W I N D O W S C R O S S P L AT F O R M PROGRESSI VE WEB APPS X
  • 8.
    HTTPS Web AppManifest Service Worker Minimum Viable Progressive Web App
  • 9.
    Web Application Manifest { "lang": "en", "short_name":"Wash Post", "name": "The Washington Post", "icons": [ { "src": "img/launcher-icon-2x.png", "sizes": "96x96", "type": "image/png" }, { "src": "img/launcher-icon-4x.png", "sizes": ”512x512", "type": "image/png" } ], "start_url": "/pwa/?utm_source=homescreen", "display": "standalone", "orientation": "portrait", "background_color": "black" }
  • 10.
    Web Application Manifest { "lang": "en", "short_name":"Wash Post", "name": "The Washington Post", "icons": [ { "src": "img/launcher-icon-2x.png", "sizes": "96x96", "type": "image/png" }, { "src": "img/launcher-icon-4x.png", "sizes": ”512x512", "type": "image/png" } ], "start_url": "/pwa/?utm_source=homescreen", "display": "standalone", "orientation": "portrait", "background_color": "black" }
  • 11.
    Web Application Manifest { "lang": "en", "short_name":"Wash Post", "name": "The Washington Post", "icons": [ { "src": "img/launcher-icon-2x.png", "sizes": "96x96", "type": "image/png" }, { "src": "img/launcher-icon-4x.png", "sizes": ”512x512", "type": "image/png" } ], "start_url": "/pwa/?utm_source=homescreen", "display": "standalone", "orientation": "portrait", "background_color": "black" }
  • 12.
    Web Application Manifest { "lang": "en", "short_name":"Wash Post", "name": "The Washington Post", "icons": [ { "src": "img/launcher-icon-2x.png", "sizes": "96x96", "type": "image/png" }, { "src": "img/launcher-icon-4x.png", "sizes": ”512x512", "type": "image/png" } ], "start_url": "/pwa/?utm_source=homescreen", "display": "standalone", "orientation": "portrait", "background_color": "black" }
  • 13.
    App display Property •Runs in Edge as a separate windows or Tab. • Full browser UI, no store listing Browser • Runs in App container in separate process. • No browser UI, back button only. • Microsoft store listing Standalone UI • Runs in App container in separate process. • Limited UI (navigation & read only nav bar). • Microsoft store listing Minimal UI
  • 14.
  • 15.
    Network request: Service Worker inplay Web App Service Worker Network Cache
  • 16.
    if ( navigator.serviceWorker) { if ( navigator.serviceWorker.controller ) { console.log('There’s an active service worker'); } else { navigator.serviceWorker.register('/sw.js') .then(function(reg) { console.log('SW registered for scope', reg.scope); }); } }
  • 17.
    if ( navigator.serviceWorker) { if ( navigator.serviceWorker.controller ) { console.log('There’s an active service worker'); } else { navigator.serviceWorker.register('/sw.js') .then(function(reg) { console.log('SW registered for scope', reg.scope); }); } } This tests to see if Service Workers are supported.
  • 18.
    if ( navigator.serviceWorker) { if ( navigator.serviceWorker.controller ) { console.log('There’s an active service worker'); } else { navigator.serviceWorker.register('/sw.js') .then(function(reg) { console.log('SW registered for scope', reg.scope); }); } } We can also see if there’s a Service Worker already running.
  • 19.
    if ( navigator.serviceWorker) { if ( navigator.serviceWorker.controller ) { console.log('There’s an active service worker'); } else { navigator.serviceWorker.register('/sw.js') .then(function(reg) { console.log('SW registered for scope', reg.scope); }); } } If there isn’t, we can register one.
  • 20.
    if ( navigator.serviceWorker) { if ( navigator.serviceWorker.controller ) { console.log('There’s an active service worker'); } else { navigator.serviceWorker.register('/sw.js') .then(function(reg) { console.log('SW registered for scope', reg.scope); }); } } By default, a Service Worker is scoped to its path.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 30.
     Validate yourPWA is ready for Microsoft Store  Improve perf and User Experience
  • 31.
  • 33.
  • 34.
  • 38.
  • 41.
    // Am Iinstalled in Windows? if ( window.Windows ){ // pin a secondary tile tile = new Windows.UI.StartScreen.SecondaryTile(tileId, text, text, activationArguments,newTileDesiredSize, logoUri); //place activation var buttonCoordinates = { x: SR.left, y: SR.top, width: SR.width, height: SR.height }; var placement = Windows.UI.Popups.Placement.above; } Access OS-level APIs Secondary Pinning
  • 42.
    PWA Activation Object Windows.UI.WebUI.WebUIApplication.addEventListener('activated',function(args) { if (args.kind === Windows.ApplicationModel.Activation.ActivationKind.voiceCommand) { var results = args.result, spoken = results.text, command = results.rulePath[0], details, redirect; if (command === 'open') { details = results.semanticInterpretation.properties.section[0]; switch (details) { case "watchlist": redirect = "https://www.mydomain.com/user/watchlist"; break; case “login": redirect = "https://www.mydomain.com/user/login"; break; … } }
  • 45.
  • 46.
    46 Section Creating the WindowsApp PWABuilder.com Changed logos Edited version info Uploaded + Submitted as Beta Integration testing VirtualBox + VM, Charles Proxy to https://localhost.twitter.com (via hosts file change, helps with HTTPS + CSP) Dev Tools Preview Edition Manually install/sideload to debug
  • 47.
    47 Section The Hard Parts -APIis massive! -Lack / inaccuracy of JS documentation and examples - Learn how to translate from C# (Class.Prop -> Class.prop) - Hard to figure out the effect of certain calls from docs (e.g. review request) -Boilerplate/config often imperative instead of config based -ASync is not native JS Promises--cannot Promise.all() - Can however just .then everything
  • 48.
    48 Section Configuration APIs /Core Path Things that interact with Windows by setting or getting variables May want them in the core render path, e.g. not code-split Usually not async => no promises!
  • 49.
    DO THIS FIRST //This must not be deferred so that it can receive the initial 'activated' event in time window.Windows.UI.WebUI.WebUIApplication.addEventListener( 'activated', e => microsoftInterfaceLoader().then(microsoftInterface => { microsoftInterface.handleActivatedEvent(e); }), false ); // Without this, the app will first refresh to the start path before every activate event window.MSApp.pageHandlesAllApplicationActivations(true);
  • 50.
    50 Read / SetUp Window
  • 51.
    51 Read / SetUp Window
  • 52.
    52 Read / SetUp Window const currentView = window.Windows.UI.ViewManagement.ApplicationView.getForCurrentView(); // Sometimes we were getting stuck in fullscreen mode because // the user had closed the app with a video open currentView.isFullScreenMode && currentView.exitFullScreenMode(); // 325px is a safe minimum size that allows the tweet layout to look OK currentView.setPreferredMinSize({ width: 325, height: 400 });
  • 53.
    53 Read / SetUp Window
  • 54.
    const isWindowsDarkMode =() => { const uiSettings = new window.Windows.UI.ViewManagement.UISettings(); const color = uiSettings.getColorValue( window.Windows.UI.ViewManagement.UIColorType.background ); return color.r === 0 && color.g === 0 && color.b === 0; }; // Prefer cookie setting, fallback to WinPWA if not set const cookies = canUseDOM ? cookie.parse(document.cookie) : {}; const cookieNightModeOn = cookies[NIGHT_MODE_COOKIE] === '1'; const windowsNightModeOn = isWindowsPwa && cookies[NIGHT_MODE_COOKIE] === undefined && isWindowsDarkMode(); (cookieNightModeOn || windowsNightModeOn) && StyleSheet.setTheme('dark'); Read / Set Up Window
  • 55.
    55 Section Feature APIs Twitter WindowsAPIs PasswordVault Share Integration** Notifications Pinning Jump Links **(for images) Store reviews / Feedback Timeline Protocol Handling **
  • 56.
  • 57.
    // Set adefault interface so elsewhere in our code we don’t have to care // what happens, we can just call this.context.hostInterface.visitedMomet let hostInterface = { onPostLoad: noop, pinnedUser: noop, visitedMoment: noop, } // If we are on Windows...load the interface for that host system... // Now some functions may be defined and actually do things! isWindowsPWA && microsoftInterfaceLoader().then(microsoftInterface => { hostInterface = { ...hostInterface, microsoftInterface } }); Code Split
  • 58.
    Share! window.navigator.share = ({text, title, url }): Promise => ???
  • 59.
    let shareData ={}; // On the windows api event, we then reattach it and format it const handleShareEvent = e => { const { text, title, url } = shareData; const request = e.request; request.data.properties.title = title; request.data.properties.description = text; const uri = new window.Windows.Foundation.Uri(url); request.data.setWebLink(uri); }; ShareShim.js
  • 60.
    ... microsoftInterface.onPostLoad = ()=> { if (window.navigator.share) { return; } // It’s a Polyfill const dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView(); dataTransferManager.addEventListener('datarequested', handleShareEvent); // Windows interface means we trigger an event and then immediately consume it // store the data here, for the event handler above to use window.navigator.share = currentShareData => { shareData = currentShareData; window.Windows.ApplicationModel.DataTransfer.DataTransferManager.showShareUI(); return Promise.resolve(); }; }; ShareShim.js
  • 64.
    Calling C# froma webview namespace CommunicationWinRT { [Windows.Foundation.Metadata.AllowForWeb] public sealed class CommunicationWinRT { public CommunicationWinRT() { } public String GetValue() { return "From my WinRT component Yo!"; } } }
  • 65.
    Calling C# froma webview document.getElementById('toast').addEventListener('click', function () { if (window.CommunicationWinRT) { testToast.toastMessage(document.getElementById('input').value, 0) } else { alert(document.getElementById('input').value) } }) if (window.CommunicationWinRT) { var testToast = new CommunicationWinRT.CommunicationWinRT(); }
  • 68.
  • 69.
    Service Worker callingWinRT APIs addEventListener('fetch', function(evt) { evt.respondWith( fromCache(evt.request) .catch(fromServer(evt.request))); evt.waitUntil(function(evt.request){ Windows.Storage.AccessCache.StorageApplicationPermissions .futureAccessList.add(evt.request); }); });
  • 72.
    Remember 1. PWAs arepart of Windows to enable Web developers to build great user experiences. 2. PWAs can naturally or specifically be added to the Microsoft store. 3. PWAs are powerful, flexible and extensible on Windows. 4. We’re not done yet!