12
8

More than 1 year has passed since last update.

【Power Pages, Power Apps】ローコード・ノーコードで誰でもWebアプリ開発 with LIFF【あらゆる人たちがデジタルの場で活躍する時代へ】

Last updated at Posted at 2022-12-24

こんにちは、もっちゃんと申します。

この記事はMicrosoft Power Apps Advent Calendar 2022 カレンダー2の24日目です。

Microsoftのテクノロジーが今、とても熱いです!
Microsoftは巨大で優れた製品群を複数有しており、世界でも類を見ない状態となっています。
古くからある製品群はもちろんのこと、Microsoft Azureのクラウドを始め、GitHub, VS Code, OS, ハードウェアに至るまで。
さらにそれらをしっかり連携させてシナジーを生み出していく構想まであります。とても熱い展開が期待できそうですね^^

この記事ではそんな熱いテクノロジーのうち、Power Pagesを使って誰でも簡単にWebアプリが作れるところを見ていきますね。Webアプリを作成するにあたり、LINEが提供しているLINE Front-end Framework (略してLIFF) というWebアプリケーションに組み込める仕組みを利用すると便利なのでこちらも組み込んでいきますね!

Microsoft Power Platform の Power Pagesとは

image.png

セキュアなビジネス Webサイトをローコードで簡単に作成する

元はPower Apps ポータル というAppsの一員でしたが、Power Pagesとして独立した形でよりパワーアップして提供されることになりました。

image.png

Power Appsの画面上でもPower Pagesに誘導される感じになっていますね。

スクリーンショット 2022-12-24 15.05.40 (1).png

先日、サティア ナデラ氏が来日して行われたイベントの中で Do more with less (より少ないリソースでより多くのことを実現) というメッセージが語られていました。
Power Pagesはまさにそういったことを実現できる優れたツール・サービスの1つと言えます。
これまで限られた専門家たちだけの領域だった開発の場が一変し、より多くの方が参入できる状況をMicrosoftが作り出しつつあります。

作り方

構成について

今回、試行錯誤した結果下記のような構成となりました。
フロント側の領域でPower Pages(Power Apps)とLINE(LIFF)が活躍し、バックエンド的なところやデータのやり取りにPower Automateを介するイメージです。

スクリーンショット 2022-12-25 9.08.36.png

LINE DevelopersでMessaging API チャネルを作成

下記を参考に作成します。
https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-console
https://developers.line.biz/ja/docs/messaging-api/building-bot/#page-title

チャネルアクセストークンをメモします。
スクリーンショット 2022-12-06 23.42.03.png

LINE DevelopersでLINE ログインチャネルの作成

下記を参考に作成します。
https://developers.line.biz/ja/docs/line-login/getting-started/#step-1-create-channel
https://developers.line.biz/ja/docs/liff/registering-liff-apps/#registering-liff-app

LIFF IDとLIFF URLをメモします。
image.png

Power Pagesの作成

アプリの作成

下記サイトにアクセスします。
https://make.powerpages.microsoft.com/

サイトを作成するをクリックします。
image.png

空のページのテンプレートを選びます。
image.png

名前とアドレスを入力し、「作成」をクリックします。
アドレスをコピーします。
image.png

LINE ログインチャネルの画面に戻り、LIFFのエンドポイントURLに先ほどのアドレスを追加します。
https://{アドレス}
image.png

アプリが作成できたら、編集画面を開きます。
image.png

サイトの表示方法の管理を開きます。
image.png

公開します。
image.png

画面をデザインします。
下記のように、テキストやボタンを追加して、作成します。

スクリーンショット 2022-12-24 14.05.37.png

GUIではできない設定を以下でコードを書いていきます。

ポータルに移動します。
image.png

ルートページのホームをクリックします。
image.png

ローカライズされたコンテンツのホームをクリックします。
image.png

もともとのコードに下記を追加したコードを加えます。
・SDKのインポート
・テキストに名前をつける(Dataverseから取得した値を入れるのに必要)
・LIFF固有の記述数行

コピー(HTML)に下記を追加し、保存します。

HTMLコード
<div class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">
  <div class="container" style="padding: 0px; display: flex; flex-wrap: wrap;">
    <div id="qrcode-url"></div>
  </div>
</div>
<div class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">
  <div class="container" style="padding: 0px; display: flex; flex-wrap: wrap; column-gap: 0px;">
    <div class="col-md-12 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 300px; margin: 20px 0px; padding: 16px; width: calc(100% - 0px);">
      <p style="color: var(--portalThemeColor2); text-align: center;">point</p>
      <p style="color: var(--portalThemeColor2); text-align: center;" id="point-num">0</p>
      <p style="color: var(--portalThemeColor2); text-align: center;">有効期限</p>
      <p style="color: var(--portalThemeColor2); text-align: center;" id="expiration-date">2023/1/1</p>
    </div>
  </div>
</div>
<div class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">
  <div class="container" style="padding: 0px; display: flex; flex-wrap: wrap;">
    <div class="col-md-12 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 300px;">
      <p>●商品購入用バーコード</p>
      <p>5秒後に商品購入シミュレーションのため、バーコード読み込みが自動的に実施されます。</p>
    </div>
  </div>
</div>
<!-- LIFF ID ERROR -->
<div id="liffIdErrorMessage" class="hidden">
  <p glot-model="liff-id-error"><!--不正なLIFFIDです。--></p>
</div>
<!-- LIFF INIT ERROR -->
<div id="liffInitErrorMessage" class="hidden">
  <p glot-model="liff-init-error"><!--liff.init()が失敗しました。--></p>
</div>
<script charset="utf-8" src="https://static.line-scdn.net/liff/edge/versions/2.8.0/sdk.js"></script>
<script src="https://unpkg.com/glottologist"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>


image.png

ちなみに、元のコードに追加した差分は下記の赤枠の部分になります。

image (2).png

Dataverseのテーブルの作成

ユーザのポイント情報を保存するテーブルを作成します。
下記のように設定し、「保存」をクリックします。

表示列
プロパティ MembersCardUserInfo
プライマリ列 userId

image.png

image.png

列を追加します。
下記のように2列追加し、「保存」をクリックします。

表示名 データの種類
point 整数
expirationDate 一行テキスト

image.png

スクリーンショット 2022-12-24 0.24.16.png

テーブルの作成は完了です。

Power Automate(フロー)の作成

下記のように新しいフローを作成します。
image.png

インスタントクラウドフローを選択します。
image.png

フロー名を入力、HTTP要求の受信時を選択し、「作成」をクリックします。
スクリーンショット 2022-12-06 13.42.56.png

image.png

「新しいステップ」をクリックします。
image.png

「変数を初期化する」と検索し、「変数を初期化する」をクリックします。
image.png

下記のように設定し、「新しいステップ」をクリックします。

設定値
名前 user_info
種類 オブジェクト

スクリーンショット 2022-12-20 15.54.04.png

こまめに保存しましょう。

同じように下記を「変数を初期化する」に設定します。

名前 種類
before_awarded_point 整数 0

次に、ブラウザから受け取ったパラメーターのJSON解析を行います。

パラメータを書き換える場合は、「properties」の中に下記の部分を書き換えます。

変数名: {
  "type": "string"
},

設定値
コンテンツ @{triggerBody()}
スキーマ 下記のJSON
jsonコード
{
    "type": "object",
    "properties": {
        "mode": {
            "type": "string"
        },
        "idToken": {
            "type": "string"
        }
    }
}

スクリーンショット 2022-12-24 0.27.09.png

次に受け取ったidTokenでユーザIDを取得します。
HTTPと検索し、HTTPを選択します。

image.png

下記のように設定します。

設定値
方法 POST
URI https://api.line.me/oauth2/v2.1/verify
ヘッダー {
  "Content-Type": "application/x-www-form-urlencoded"
  }
本文 id_token=@{body('JSON_の解析')?['idToken']}&client_id={LIFFでコピーしたLIFF IDの数字部分}

スクリーンショット 2022-12-24 0.35.54.png

次にDataverseからユーザーのデータがあるか検索します。
「行を一覧にする」と検索し、「行を一覧にする」をクリックします。
image.png

下記のように設定します。

設定値
テーブル名 MembersCardUserInfo
列を選択する cr08b_userid,cr08b_point,cr08b_expirationdate,cr08b_memberscarduserinfoid
行のフィルター cr08b_userid eq '@{body('HTTP')?['sub']}'

スクリーンショット 2022-12-24 0.37.07.png

処理を分けて作成しまていきます。
「スイッチ」をクリックします。
image.png

設定値
オン @{body('JSON_の解析')?['mode']}
ケース init
ケース2 buy

+ボタンでケースを増やします。

image.png

【init 側】

「条件」を追加し、ユーザのデータがあるかないかで切り分けます。

設定値
左の値 @{length(outputs('行を一覧にする')?['body/value'])}
中央の値 次の値に等しい
右の値 0

スクリーンショット 2022-12-20 16.37.36.png

[はい の側]

下記を設定し、Dataverseに行を追加します。

設定値
テーブル名 MembersCardUserInfo
userId @{body('HTTP')?['sub']}
point 0

スクリーンショット 2022-12-24 0.38.56.png

次に画面に返す値を設定します。

設定値
名前 user_info
下記のJSON
jsonコード
{
  "userId": @{outputs('新しい行を追加する')?['body/cr08b_userid']},
  "pointExpirationDate": @{outputs('新しい行を追加する')?['body/cr08b_expirationdate']},
  "point": @{outputs('新しい行を追加する')?['body/cr08b_point']},
  "id": @{outputs('新しい行を追加する')?['body/cr08b_memberscarduserinfoid']}
}

スクリーンショット 2022-12-24 0.43.15.png

[いいえ 側]

ユーザのデータを画面に返す値を設定します。
「それぞれに適用する」をクリックします。

image.png

下記を設定します。

設定値
以前の手順から出力を選択 @{outputs('行を一覧にする')?['body/value']}

変数を設定します。

設定値
名前 user_info
下記のJSON
jsonコード
{
  "userId": @{items('Apply_to_each')?['cr08b_userid']},
  "pointExpirationDate": @{items('Apply_to_each')?['cr08b_expirationdate']},
  "point": @{items('Apply_to_each')?['cr08b_point']},
  "id": @{items('Apply_to_each')?['cr08b_memberscarduserinfoid']}
}

スクリーンショット 2022-12-24 0.43.52.png

【buy 側】

変数の設定をします。

設定値
名前 before_awarded_point
@{items('Apply_to_each_2')?['cr08b_point']}

自動的に「Apply to each 2」が作られます。

スクリーンショット 2022-12-21 13.35.27.png

Dataveseに行を更新します。

設定値
テーブル MembersCardUserInfo
行ID @{items('Apply_to_each_2')?['cr08b_memberscarduserinfoid']}
userId @{items('Apply_to_each_2')?['cr08b_userid']}
expirationDate @{addDays(utcNow(),365,'yyyy/MM/dd')}
point @{add(variables('before_awarded_point'),5)}

スクリーンショット 2022-12-24 0.45.01.png

変数を設定します。

設定値
名前 user_info
下記のJSON
jsonコード
{
  "userId": @{outputs('行を更新する')?['body/cr08b_userid']},
  "pointExpirationDate": @{outputs('行を更新する')?['body/cr08b_expirationdate']},
  "point": @{outputs('行を更新する')?['body/cr08b_point']},
  "id": @{outputs('行を更新する')?['body/cr08b_memberscarduserinfoid']}
}

HTTPを追加し、トーク画面へpushを使って、メッセージを送信します。

設定値
方法 POST
URI https://api.line.me/v2/bot/message/push
ヘッダー {
  "Content-Type": "application/json",
  "Authorization": "Bearer {LINE側でメモしたアクセストークン}
  }
本文 下記JSON
jsonコード
{
  "to": "@{outputs('行を更新する')?['body/cr08b_userid']}",
  "messages": [
    {
      "type": "flex",
      "altText": "お買い上げありがとうございます。電子レシートを発行します。",
      "contents": {
        "type": "bubble",
        "header": {
          "type": "box",
          "layout": "vertical",
          "contents": [
            {
              "type": "text",
              "text": "領収書",
              "size": "xxl",
              "weight": "bold"
            },
            {
              "type": "text",
              "text": "@{utcNow()}",
              "color": "#767676"
            },
            {
              "type": "text",
              "wrap": true,
              "text": "※デジタル会員証のハンズオンアプリであるため、実際の課金は行われません",
              "color": "#ff6347"
            }
          ]
        },
        "body": {
          "type": "box",
          "layout": "vertical",
          "contents": [
            {
              "type": "box",
              "layout": "vertical",
              "margin": "lg",
              "spacing": "sm",
              "contents": [
                {
                  "type": "box",
                  "layout": "baseline",
                  "spacing": "sm",
                  "contents": [
                    {
                      "type": "text",
                      "text": "チョコレート",
                      "color": "#5B5B5B",
                      "size": "sm",
                      "flex": 5
                    },
                    {
                      "type": "text",
                      "text": "500",
                      "wrap": true,
                      "color": "#666666",
                      "size": "sm",
                      "flex": 2,
                      "align": "end"
                    }
                  ]
                },
                {
                  "type": "box",
                  "layout": "baseline",
                  "spacing": "sm",
                  "contents": [
                    {
                      "type": "text",
                      "text": "お会計金額",
                      "color": "#5B5B5B",
                      "size": "sm",
                      "flex": 5
                    },
                    {
                      "type": "text",
                      "text": "500",
                      "wrap": true,
                      "color": "#666666",
                      "size": "sm",
                      "flex": 2,
                      "align": "end"
                    }
                  ]
                },
                {
                  "type": "box",
                  "layout": "baseline",
                  "spacing": "sm",
                  "contents": [
                    {
                      "type": "text",
                      "text": "付与ポイント",
                      "color": "#5B5B5B",
                      "size": "sm",
                      "flex": 5
                    },
                    {
                      "type": "text",
                      "text": "5",
                      "wrap": true,
                      "color": "#666666",
                      "size": "sm",
                      "flex": 2,
                      "align": "end"
                    }
                  ]
                }
              ],
              "paddingBottom": "xxl"
            },
            {
              "type": "box",
              "layout": "vertical",
              "contents": [
                {
                  "type": "text",
                  "text": "商品のご購入ありがとうございます。",
                  "wrap": true,
                  "size": "sm",
                  "color": "#767676"
                }
              ]
            }
          ],
          "paddingTop": "0%"
        }
      }
    }
  ]
}

buy側は完了です。

最後にスイッチの下に応答を追加し、値を画面に返します。

設定値
状態コード 200
ヘッダー {
  "Content-Type": "application/json"
  }
本文 @{variables('user_info')}

image.png

Automateの作成は完了です。

アプリの作成のつづき

下記の画面に戻り、カスタムJavascriptに下記を追加し、保存します。

Javascriptコード

下記に書き換えます。
BASE_URL: {Power AutomateでコピーしたHTTP POST のURL}
liffId: {LIFFでコピーしたLIFF ID}

  // 環境設定を読み込む
  const BASE_URL = "xxxxxx";
  const liffId = "xxx-xxx"

  // グローバル変数の宣言
  let idToken = "";
  let lang = "ja";

  //多言語対応のメッセージ読み込み
  let message = {}
  
  window.onload = function () {
    let myLiffId = liffId;
    initializeLiffOrDie(myLiffId);
  };

  /**
   * Check if myLiffId is null. If null do not initiate liff.
   * @param {string} myLiffId The LIFF ID of the selected element
   */
  function initializeLiffOrDie(myLiffId) {
    if (!myLiffId) {
      document.getElementById("liffIdErrorMessage").classList.remove("hidden");
    } else {
      initializeLiff(myLiffId);
    }
  }

  /**
   * Initialize LIFF
   * @param {string} myLiffId The LIFF ID of the selected element
   */
  function initializeLiff(myLiffId) {
    liff
      .init({
        liffId: myLiffId,
      })
      .then(() => {
        // start to use LIFF's api
        initializeApp();
      })
      .catch((err) => {
        document
          .getElementById("liffInitErrorMessage")
          .classList.remove("hidden");
      });
  }

  function initializeApp() {
    if (!liff.isLoggedIn()) {
      liff.login({redirectUri: location.href});
    }

    idToken = liff.getIDToken();
    getUserData(idToken);
    setTimeout(() => demoAddPoint(), 5000);
  }

  /**
   * ログインユーザーの会員データを取得する
   * @param {String} idToken
   */
  function getUserData(idToken) {
    const body = {
      "mode": "init",
      "idToken": idToken,
    };
    // URLを開く
    let request = new XMLHttpRequest();
    request.open("POST", BASE_URL, true);
    request.responseType = "json";
    request.onload = function () {
      console.log(request.status)
      if (request.readyState === 4 && request.status === 200) {
        data = this.response; 
        displayPoint(data.point);
        displayExpirationDate(data.pointExpirationDate);
        setQrcodeUrl("https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl="+ data.userId)
        
      } else {
        console.warn(message.serverError[lang]);
      }
    };

    request.send(JSON.stringify(body));
  }

  /**
   * 画面にポイントを表示する
   * @param {String} point
   */
  function displayPoint(point) {
    $("#point-num").text(point);
  }

  /**
   * QRコードにURLを埋める
   * @param {String} point
   */
  function setQrcodeUrl(url) {
    var base;
	var obj;

	base = document.getElementById("qrcode-url");
	base.innerHTML = "";

    // IFRAME 作成
    obj = document.createElement("iframe");
    // IFRAME の見栄え属性をセット
    obj.setAttribute("frameBorder", "0");

    // IFRAME の配置属性をセット
    obj.style.position = "relative";
    obj.style.width = "200px";
    obj.style.height = "200px";

    // IFRAME の内容をセット
    obj.src = url;

    // IFRAME を実装
    base.appendChild(obj);
  }

  /**
   * 画面にポイント期限日を表示する
   * @param {String} expirationDate
   */
  function displayExpirationDate(expirationDate) {
    $("#expiration-date").text(expirationDate);
  }

  function demoAddPoint() {
    const body = {
      mode: "buy",
      idToken: idToken
    };
    
    let request = new XMLHttpRequest();
    request.open("POST", BASE_URL, true);
    request.responseType = "json";

    request.onload = function () {
      if (request.readyState === 4 && request.status === 200) {
        data = this.response;
        displayPoint(data.point);
        displayExpirationDate(data.pointExpirationDate);
      } else if(request.status === 403) {
        if(!alert(message.sessionExpired[lang])){
          liff.logout();
          liff.login({redirectUri: location.href});
        }
      } else {
        alert(message.error[lang]);
      }
    };

    request.send(JSON.stringify(body));
  }

※ 本当はPagesとDataverseのよりシームレスな連携の模索をもっとしたかったですが、いまはPower Automateのエンドポイントを実行する形で着地しています

動作確認

pages-2.gif

お買い物のシーンでデジタル会員証が大活躍❗️
お会計時にスマホのカメラでデジタル会員証を簡単に開くことができ、店員さんにQRコードをよんでもらってポイントが付与されるという紙を一切使用しない環境にも優しい仕組みです✨

このデモ動画ではQRコードをよんでくれる店員さんはいないので5秒たったらQRコードをよんでくれた体にしています笑

まとめ

Webアプリケーションは現在主力のアプリケーション形式の1つかと思います。
従来ですと、限られた専門家だけがWebアプリの開発を行うことができていましたが、Microsoftのテクノロジーはその従来の概念を打ち砕き、新しい展開を切り拓いていくのではないでしょうか?

現時点ではまだ全てのWebアプリを作れるとまでは言えないかもしれませんが、今後に非常に期待がもてる機能だと言えそうですね!これからのアップデートが楽しみです✨

参考

https://learn.microsoft.com/ja-jp/power-apps/maker/portals/create-portal
https://learn.microsoft.com/ja-jp/power-apps/maker/portals/compose-page
https://developers.google.com/chart/infographics/docs/qr_codes

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8