PWA 操作 Windows Runtime APIs

Benefits of PWA on Windows 10s 介紹了 PWA 在 Windows 10 的好處,讓我更想知道開發 PWA 的細節。

這篇介紹使用 Windows Runtime APIs 的範例。

在開發前,有兩個方式可以方便 debug:

  1. 建立 Progressive Web App (Universal Windows),設定 Start Page 與 WindowsRuntimeAccess = true 如下:
    <Application Id="App" StartPage="http://localhost:65278/">
      <uap:ApplicationContentUriRules>
        <uap:Rule Match="http://localhost:65278/" Type="include" WindowsRuntimeAccess="all" />
        <uap:Rule Match="https://*.*" Type="include" WindowsRuntimeAccess="none" />
        <uap:Rule Match="http://*.*" Type="include" WindowsRuntimeAccess="none" />
      </uap:ApplicationContentUriRules>  
    </Application>
    有宣告 WindowsRuntimeAccess 才能操作 Windows Runtime APIs。我比較建議使用這個方式。
  2. 利用 Microsoft Edge Developer Tools 幫助我們開發 PWA 與確定那些 APIs 是否能使用

利用 JavaScript 開發與使用 WinRT APIs 與 C# 大部分相同,但需要注意:

  • WinRT features 在 Javascript 使用時命名大小寫有些不同,可參考 Casing Conventions with Windows Runtime Features
  • 事件注冊改用 string 名稱搭配 addEventListenerremoveEventListener 來處理,或是event = function(ev) {} 的寫法;
    var locator = new Windows.Devices.Geolocation.Geolocator();  
    locator.addEventListener(  
        "positionchanged",   
         function (ev) {  
            console.log("Got event");  
        });
  • 非同步執行模式使用 JavaScript Promise model,寫法如下:
    client.createResourceAsync(uri, description, item)  
        // Success.  
        .then(function(newItem) {   
            console.log("New item is: " + newItem.id);  
                });
    更多詳細的寫法可參考:Asynchronous programming in JavaScript
  • Windows.UI.Xaml 不支援 JavaScript apps,改用 EdageHTML engine 來渲染 CSS 與 HTML
  • Windows.UI.WebUI.WebUIApplication 負責 App 生命周期與相關事件,非常重要。App activated, resume, and suspend using the WRL sampleApp activate and suspend using WinJS sample 介紹怎麽操作 actived, suspended 的事件。
    Windows.UI.WebUI.WebUIApplication.addEventListener("activated", function (activatedEventArgs) {
        // Check activatedEventArgs.kind and respond as needed
    });
  • 利用 if(window.Windows){} 檢查是否支援 Windows Runtime APIs;或者是多檢查特定的 API:if (window.Windows && Windows.UI.Popups)

更多細節可以參考 Using the Windows Runtime in JavaScript 或是下載範例 Windows-universal-samples 中的 JS 範例。

另外可參考 Windows UWP Namespaces 與搭配 Universal Windows Platform documentation 補充 UWP 開發的基本觀念。

列出常用到 PWA 的功能:

1. 如何支援 Push Notification

Windows Push Notification 的機制可以參考之前的文章:Universal App - 整合 Windows Notification Service (WNS) for ServerUniversal App - 整合 Windows Notification Service (WNS) for Client

PWA 中注冊取得 WNS channel uri

  1. 先到 Microsoft Dev Center 的專案啓動 WNS 服務,如下圖:
    Server side 建議參考sample code來測試;
  2. 利用 VS2017 讓 PWA app 關聯到 Microsoft Dev Center 注冊的專案,更新 package.appxmanifest 的參數;
    重點在:<Identity Name="PouMason.PROJECT1" Publisher="CN=4CABE714-48E4-AC4F-7F16-A5774B167C16F8" Version="1.1.2.0" />
  3. 加入 JavaScript 操作 CreatePushNotificationChannelForApplicationAsync 來得到 channel_uri;
    var wnsChannel;
    
    // 注冊 push notification 
    if (typeof Windows !== 'undefined' &&
        typeof Windows.UI !== 'undefined' &&
        typeof Windows.UI.Notifications !== 'undefined') {
    
        Windows.Networking.PushNotifications.PushNotificationChannelManager.createPushNotificationChannelForApplicationAsync()
            .then(function (newChannel) {
                console.log(newChannel);
                wnsChannel = newChannel;
                wnsChannel.addEventListener("pushnotificationreceived", pushNotificationReceivedHandler, false);
            }, function (error) {
                console.log(error);
            });
    }
  4. 處理收到的訊息:
    function pushNotificationReceivedHandler(e) {
        var notificationTypeName = "";
        var notificationPayload;
        var pushNotifications = Windows.Networking.PushNotifications;
    
        // 拆解對應的類型
        switch (e.notificationType) {
            case pushNotifications.PushNotificationType.toast:
                notificationTypeName = "Toast";
                notificationPayload = e.toastNotification.content.getXml();
                break;
            case pushNotifications.PushNotificationType.tile:
                notificationTypeName = "Tile";
                notificationPayload = e.tileNotification.content.getXml();
                break;
            case pushNotifications.PushNotificationType.badge:
                notificationTypeName = "Badge";
                notificationPayload = e.badgeNotification.content.getXml();
                break;
        }
    
        e.cancel = true;
        // 取得 payload 中的内容
        var xmlDox = new Windows.Data.Xml.Dom.XmlDocument();
        xmlDox.loadXml(notificationPayload);
        var textElements = xmlDox.getElementsByTagName("text")
    
        var messageDialog = new Windows.UI.Popups.MessageDialog(notificationTypeName, textElements[0].innerText);
        messageDialog.showAsync();
    }

如果有開發好的 server,可以在拿到 channel_uri 轉送到 server 保存起來,這裏的範例就不特別説明這一段。

關於 JavaScript 注冊與處理 Push Notification 的事件可以參考 PushNotificationReceivedEventArgs Class

另外,如果想要讓 App 開啓時收到 Push notification 就做處理,建議可以把注冊的邏輯放到 Service Worker 裏面,讓背景來處理。

更多細節可參考 Push notifications in a PWA running on Windows 10 的範例。

2. 在 PWA 中發送 Toast

function (message, iconUrl) {
    // 檢查是否支援 Windows Runtime API
    if (typeof Windows !== 'undefined' &&
        typeof Windows.UI !== 'undefined' &&
        typeof Windows.UI.Notifications !== 'undefined') {
        
        var notifications = Windows.UI.Notifications;
        // 利用 ToastTemplateType 列舉選擇要用的範本
        var template = notifications.ToastTemplateType.toastImageAndText01;
        // 轉成 XML
        var toastXml = notifications.ToastNotificationManager.getTemplateContent(template);

        var textElements = toastXml.getElementsByTagName("text");
        textElements[0].appendChild(toastXml.createTextNode(message));
        var imageElements = toastXml.getElementsByTagName("image");
        // 設定 image 的 src 屬性
        var srcAttr = toastXml.createAttribute("src");
        srcAttr.value = iconUrl;
        var attribs = imageElements[0].attributes;
        attribs.setNamedItem(srcAttr);

        // 建立 toast 並發送
        var toast = new notifications.ToastNotification(toastXml);
        var toastNotifier = notifications.ToastNotificationManager.createToastNotifier();
        toastNotifier.show(toast);
    }
}

可以根據需求 ToastTemplateType Enum 調整需要的範本。

3. 在 App 的 Local folder 讀寫檔案

參考 ApplicationData Class 來操作: 讀取檔案

function (fileName) {
    if (typeof Windows !== 'undefined' &&
        typeof Windows.Storage !== 'undefined' &&
        typeof Windows.Storage.ApplicationData !== 'undefined') {

        var localFolder = Windows.Storage.ApplicationData.current.localFolder;

        localFolder.getFileAsync(fileName)
            .then(function (file) {
                // 抓到檔案並讀取
                return Windows.Storage.FileIO.readTextAsync(file);
            }, function (ex) {
                console.log(ex);
            })
            .done(function (content) {
                console.log(content);
            });
    }
}

寫入檔案

function (fileName, content) {
    if (typeof Windows !== 'undefined' &&
        typeof Windows.Storage !== 'undefined' &&
        typeof Windows.Storage.ApplicationData !== 'undefined') {
        
        var localFolder = Windows.Storage.ApplicationData.current.localFolder;

        localFolder.createFileAsync(fileName, Windows.Storage.CreationCollisionOption.replaceExisting)
            .then(function (file) {
                // 抓到檔案並寫入
                return Windows.Storage.FileIO.writeTextAsync(file, content);
            })
            .done(function () {
                console.log("saved.");
            });
    }
}

4. 操作 User Activity

在 time line 加入 activity

function (activityId) {
    if (typeof Windows !== 'undefined' &&
        typeof Windows.ApplicationModel !== 'undefined' &&
        typeof Windows.ApplicationModel.UserActivities !== 'undefined') {

        createActivity(activityId).the(function () {
            console.log("done");
        });
    }
};

async function createActivity(activityId) {
    var channel =  Windows.ApplicationModel.UserActivities.UserActivityChannel.getDefault();
    var activity = await channel.getOrCreateUserActivityAsync(activityId);

    if (activity.state == Windows.ApplicationModel.UserActivities.UserActivityState.new) {
        activity.visualElements.displayText = "new activity";
        activity.activationUri = new Windows.Foundation.Uri('testapp://mainPage?state=new&id=' + activityId);
    } else {
        activity.visualElements.displayText = "published activity";
        activity.activationUri = new Windows.Foundation.Uri('testapp://mainPage?state=published&id=' + activityId);
    }

    activity.contentInfo = Windows.ApplicationModel.UserActivities.UserActivityContentInfo.fromJson('{ "user_id": "pou", "email": "poumason@live.com"}');

    await activity.saveAsync();

    var activitySesion = activity.createSession();
}

利用 async function 處理 then 裏面還有非同步方法的複雜寫法。

處理點擊 User Activity 帶入的 protocol

if (typeof Windows !== 'undefined' &&
    typeof Windows.UI !== 'undefined' &&
    typeof Windows.UI.WebUI !== 'undefined') {

    Windows.UI.WebUI.WebUIApplication.addEventListener("activated", function (activatedEventArgs) {
        console.log(activatedEventArgs);

        if (activatedEventArgs.kind == Windows.ApplicationModel.Activation.ActivationKind.protocol ) {
            var query = activatedEventArgs.uri.queryParsed;
            console.log(query);

            for (var i = 0; i < query.length; i++) {
                console.log(query[i].name + '=' + query[i].value);
            }
        }
    });
}

[範例]

DotblogsSampleCode/27-PWASample/

======

以上是我研究 PWA 使用 Windows Runtime APIs 的心得,希望有助於大家快速理解。 如果有寫錯的地方,也歡迎大家留言讓我知道,謝謝大家。

References