33
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

スマホやPCのカメラでQRを連続して読み取り、スプレッドシートに記録するApps Scriptのウェブアプリ

Last updated at Posted at 2022-02-23

JavaScriptのライブラリー html5-qrcode を使えば、スマートフォンやPCのカメラを使ってQRコード1を連続して読み取り、スプレッドシートに記録するウェブアプリを無料で作れます。バーコードもOKです🚀

デモに使ったスプレッドシートとApps Scriptはこちらです。どうぞコピーしてご自由にお使いください2。なお、Google Workspace(for Education)を使っている企業(学校)の方は、ブラウザーをシークレットモードにしてアクセスしてみてください。

使っている技術については後述していきます。

ちなみに、GoogleのノーコードツールAppSheetを使えば、連続読み取りはできないものの、QRコードを使うアプリを簡単に作れます。よろしければご覧ください。

注意事項

QRコードの読み取りは、端末やカメラの性能、光源などによって大きく左右されます。

QRコードリーダーとして使いたいスマホやPCと、読み取らせたいQRコードを準備の上、次のcodepen.ioのURLにアクセスし、期待通りの読み取りができることをお試しください。読み取りが成功すると、音が出ます。

使い方

スプレッドシートをコピーして、Apps Scriptをウェブアプリとして公開します。

  • スプレッドシートにアクセスして、上部メニュー「ファイル」>「コピーを作成」でコピーします。Apps Scriptも一緒にコピーされます。
  • コピーしたスプレッドシートの上部メニュー「拡張機能」>「Apps Script」でスクリプトエディターを開きます。
  • Apps Scriptのスクリプトエディター右上にある「デプロイ」ボタンから「新しいデプロイ」を選び、アプリの種類で「ウェブアプリ」を選択の上、デプロイします。
  • 権限が求められたら付与してください。

URLが払い出されるので、PCもしくはスマートフォンのウェブブラウザーからアクセスしてください。

  • アクセスしたら、Request Camera Permissions ボタンを押して、カメラを許可します。
  • QRコードを読み取ると、スプレッドシートのシート「Data」に、日付と読み取り結果を追記していきます。
  • iOSで音を鳴らす場合は、一度Sound ON (iOS)ボタンを押してください。Androidは何もしなくても音が鳴ります。

なお、スクリプトを書き換えた場合は、スクリプトエディターで再デプロイが必要になりますのでご注意ください(後述します)。

使っている技術

複数の技術を組み合わせて実現しています。

  • バックエンド
    • Google Apps Script
      • doGet() を使ったウェブアプリ化
      • スプレッドシートへのデータ追加
  • フロントエンド
    • html5-qrcode
      • QRコードのスキャン
    • HTMLAudioElement
      • 読み取り音の再生
    • Google Apps Script
      • google.script.run() を使ったバックエンドのAPI呼び出し
  • その他
    • IMAGE関数と、Google Charts API
      • QRコードの作成

バックエンド

Apps Script は次の通りです。

Code.gs
const SHEET_NAME = 'Data';

function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

function onScan(text) {
  SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(SHEET_NAME).appendRow([new Date(), text]);
}

後述しますが、HTMLファイルindexをApps Scriptに追加しています。

doGet()で、アクセスしてきたウェブブラウザーにindexを渡します。スマートフォンに対応するため、addMetaTag()で画面が拡大しないよう調整しています。

onScan(text)はフロントエンドから呼び出します。スプレッドシートに日付と受け取ったデータtextを追記します。

ウェブアプリの再デプロイ

ウェブアプリには2種類のURLがあります。「あれ?スクリプトを書き直したのに反映されないな?」と思ったら、URLを確認してみてください。

  • 開発用
    • 末尾が/devのURLです。
    • 「新しいデプロイ」の後、「デプロイをテスト」で表示されるURLです。
    • スクリプトエディターで保存したスクリプトがそのまま動きます。
  • 本番用
    • 末尾が/execのURLです。
    • 「新しいデプロイ」や「スクリプトを管理」で表示されるURLです。
    • 再デプロイした時点のスクリプトで動作します。

開発用のURLで正しく動作することを確認したら、再デプロイをして、本番用の動作を変更します。再デプロイするには「デプロイを管理」を使います。

  • スクリプトエディター右上の「デプロイ」>「デプロイを管理」を選択
  • 鉛筆アイコンを押して、バージョンを「新しいバージョン」にして、デプロイ

このやり方なら、新しいデプロイで払い出されたURLはそのままで、動作を変更していくことができます。

フロントエンド

Apps Scriptは次の通りです。

index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <div id="reader" style="width:340px;"></div>
    <button id="sound">Sound ON (iOS)</button>
    <div id="result"></div>
    <script src="https://unpkg.com/html5-qrcode"></script>
    <script>
let CURRENT_TEXT = '';

const append = (text) => {
  document.querySelector('#result').innerHTML += `${text}<br>`; 
};

const onScanSuccess = (decodedText, decodedResult) => {
  console.log('onScanSuccess', decodedText, CURRENT_TEXT);
  if (CURRENT_TEXT !== decodedText) {
    CURRENT_TEXT = decodedText;
    append(decodedText);
    beep();
    google.script.run
      .withFailureHandler(function (error) {
        append(`"${decodedText}"の保存に失敗しました: ${error}`);
      })
      .onScan(decodedText);
  }
};

const html5QrcodeScanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: 240 });
html5QrcodeScanner.render(onScanSuccess);

const DATA_URL = "data:audio/wav;base64,//uQRAAAAWMSLwUIYAAsYkXgoQwAEaYLWfkWgAI0wWs/ItAAAGDgYtAgAyN+QWaAAihwMWm4G8QQRDiMcCBcH3Cc+CDv/7xA4Tvh9Rz/y8QADBwMWgQAZG/ILNAARQ4GLTcDeIIIhxGOBAuD7hOfBB3/94gcJ3w+o5/5eIAIAAAVwWgQAVQ2ORaIQwEMAJiDg95G4nQL7mQVWI6GwRcfsZAcsKkJvxgxEjzFUgfHoSQ9Qq7KNwqHwuB13MA4a1q/DmBrHgPcmjiGoh//EwC5nGPEmS4RcfkVKOhJf+WOgoxJclFz3kgn//dBA+ya1GhurNn8zb//9NNutNuhz31f////9vt///z+IdAEAAAK4LQIAKobHItEIYCGAExBwe8jcToF9zIKrEdDYIuP2MgOWFSE34wYiR5iqQPj0JIeoVdlG4VD4XA67mAcNa1fhzA1jwHuTRxDUQ//iYBczjHiTJcIuPyKlHQkv/LHQUYkuSi57yQT//uggfZNajQ3Vmz+Zt//+mm3Wm3Q576v////+32///5/EOgAAADVghQAAAAA//uQZAUAB1WI0PZugAAAAAoQwAAAEk3nRd2qAAAAACiDgAAAAAAABCqEEQRLCgwpBGMlJkIz8jKhGvj4k6jzRnqasNKIeoh5gI7BJaC1A1AoNBjJgbyApVS4IDlZgDU5WUAxEKDNmmALHzZp0Fkz1FMTmGFl1FMEyodIavcCAUHDWrKAIA4aa2oCgILEBupZgHvAhEBcZ6joQBxS76AgccrFlczBvKLC0QI2cBoCFvfTDAo7eoOQInqDPBtvrDEZBNYN5xwNwxQRfw8ZQ5wQVLvO8OYU+mHvFLlDh05Mdg7BT6YrRPpCBznMB2r//xKJjyyOh+cImr2/4doscwD6neZjuZR4AgAABYAAAABy1xcdQtxYBYYZdifkUDgzzXaXn98Z0oi9ILU5mBjFANmRwlVJ3/6jYDAmxaiDG3/6xjQQCCKkRb/6kg/wW+kSJ5//rLobkLSiKmqP/0ikJuDaSaSf/6JiLYLEYnW/+kXg1WRVJL/9EmQ1YZIsv/6Qzwy5qk7/+tEU0nkls3/zIUMPKNX/6yZLf+kFgAfgGyLFAUwY//uQZAUABcd5UiNPVXAAAApAAAAAE0VZQKw9ISAAACgAAAAAVQIygIElVrFkBS+Jhi+EAuu+lKAkYUEIsmEAEoMeDmCETMvfSHTGkF5RWH7kz/ESHWPAq/kcCRhqBtMdokPdM7vil7RG98A2sc7zO6ZvTdM7pmOUAZTnJW+NXxqmd41dqJ6mLTXxrPpnV8avaIf5SvL7pndPvPpndJR9Kuu8fePvuiuhorgWjp7Mf/PRjxcFCPDkW31srioCExivv9lcwKEaHsf/7ow2Fl1T/9RkXgEhYElAoCLFtMArxwivDJJ+bR1HTKJdlEoTELCIqgEwVGSQ+hIm0NbK8WXcTEI0UPoa2NbG4y2K00JEWbZavJXkYaqo9CRHS55FcZTjKEk3NKoCYUnSQ0rWxrZbFKbKIhOKPZe1cJKzZSaQrIyULHDZmV5K4xySsDRKWOruanGtjLJXFEmwaIbDLX0hIPBUQPVFVkQkDoUNfSoDgQGKPekoxeGzA4DUvnn4bxzcZrtJyipKfPNy5w+9lnXwgqsiyHNeSVpemw4bWb9psYeq//uQZBoABQt4yMVxYAIAAAkQoAAAHvYpL5m6AAgAACXDAAAAD59jblTirQe9upFsmZbpMudy7Lz1X1DYsxOOSWpfPqNX2WqktK0DMvuGwlbNj44TleLPQ+Gsfb+GOWOKJoIrWb3cIMeeON6lz2umTqMXV8Mj30yWPpjoSa9ujK8SyeJP5y5mOW1D6hvLepeveEAEDo0mgCRClOEgANv3B9a6fikgUSu/DmAMATrGx7nng5p5iimPNZsfQLYB2sDLIkzRKZOHGAaUyDcpFBSLG9MCQALgAIgQs2YunOszLSAyQYPVC2YdGGeHD2dTdJk1pAHGAWDjnkcLKFymS3RQZTInzySoBwMG0QueC3gMsCEYxUqlrcxK6k1LQQcsmyYeQPdC2YfuGPASCBkcVMQQqpVJshui1tkXQJQV0OXGAZMXSOEEBRirXbVRQW7ugq7IM7rPWSZyDlM3IuNEkxzCOJ0ny2ThNkyRai1b6ev//3dzNGzNb//4uAvHT5sURcZCFcuKLhOFs8mLAAEAt4UWAAIABAAAAAB4qbHo0tIjVkUU//uQZAwABfSFz3ZqQAAAAAngwAAAE1HjMp2qAAAAACZDgAAAD5UkTE1UgZEUExqYynN1qZvqIOREEFmBcJQkwdxiFtw0qEOkGYfRDifBui9MQg4QAHAqWtAWHoCxu1Yf4VfWLPIM2mHDFsbQEVGwyqQoQcwnfHeIkNt9YnkiaS1oizycqJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=";  

const AUDIO = new Audio();
const beep = () => {
  AUDIO.src = DATA_URL;
}

// for iOS audio
// https://stackoverflow.com/questions/31776548/why-cant-javascript-play-audio-files-on-iphone-safari
AUDIO.autoplay = true;
document.querySelector("#sound").addEventListener("click", () => {
  beep();
});
    </script>
  </body>
</html>

<div id="reader">にQRコードリーダーを表示し、<div id="result">に読み取った文字を画面に追記していきます。追記にはappend()を使います。

<script src="https://unpkg.com/html5-qrcode"></script>でhtml5-qrcodeを読み込みます。

CURRENT_TEXTは2度読み防止のための変数です。

onScanSuccess()はQRコードの読み取りが成功したときの処理です。CURRENT_TEXTと違う文字だった場合は、新しい読み取りです。append()で文字を追記し、beep()で音をならし、google.script.runで文字をバックエンドに伝えます。

HTMLAudioElementで音を鳴らす

HTMLAudioElementで音を鳴らせます。音のデータをデータURLとして文字列にエンコードしておき、先に作っておいたAudioオブジェクトのsrc属性にデータURLを代入します。

当初はnew Audio(DATA_URL).play()で再生していたのですが(Androidでは再生できるものの)iOSでは再生できないことが分かりました。iOSでは自動再生はできず、ユーザーが操作した時にだけ音を再生します。ボタンが押されたタイミングでAudioオブジェクトで音を鳴らし、そのオブジェクトを使いまわすことで、ボタンが押された以降は音を再生できるようにしています。

google.script.runでバックエンドのAPIを呼び出す

google.script.runを使って、バックエンドのAPIを非同期に呼び出します。withFailureHandler()で呼び出しに失敗したときの処理を書いています。

その他

知っておくと便利なTipsを紹介します。

GoogleスプレッドシートにQRコードを表示する

スプレッドシートを使って、QRコードをお手軽に作ることができます。

例えば、A1セルに入った文字をQRコードとして表示するには、次のように書きます。ウェブページにアクセスするためのQRコードを作るときには、ENCODEURL関数を使うようにしてください。

=IMAGE("https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl="&ENCODEURL(A1))

【応用編】QRコードリーダーにプルダウンメニューを追加して、QRコードと一緒に記録する

QRコードリーダーを改造して機能追加していく様子を、順を追って説明します(最後に完成したスクリプト一式を掲載します)。ここでは、QRコードリーダーにプルダウンメニューを追加し、メニューで選んだ項目を読み取ったQRコードと一緒に記録させてみます。

image.png

上図の場合、読み取ったQRコードhttps://ja.wikipedia.orgメニュー1をスプレッドシートに追記します。

フロントエンドにメニューを直接書き込む

まず、フロントエンドのindex.htmlにプルダウンメニューを追加し、項目として、メニュー1メニュー2メニュー3を表示します。なお、これまでは読み取ったQRコードの二度読みを禁止してきましたが、メニューを変更した場合は二度読みを可能にします。

index.html
    ...
    <div id="result"></div>

    <!-- メニューを変更したら読み取り結果をリセットして二度読みを可能にする -->
    <select id="menu" onchange="CURRENT_TEXT = '';">
      <option>メニュー1</option>
      <option>メニュー2</option>
      <option>メニュー3</option>
    </select>

    <script src="https://unpkg.com/html5-qrcode"></script>
    ...

また、選択した項目を、読み取ったQRコードと一緒にバックエンドに送信します。バックエンド側のonScan()は、引数を1つ増やします。

index.html
const onScanSuccess = (decodedText, decodedResult) => {
  ...
    beep();
    // 選択したメニューを取得
    const menu = document.querySelector('#menu option:checked').innerText;
    google.script.run
      .withFailureHandler(function (error) {
        append(`"${decodedText}"の保存に失敗しました: ${error}`);
      })
      .onScan(decodedText, menu); // 選択したメニューをスプレッドシートに送信
  }
};

バックエンド側のonScan()です。スプレッドシートに追加する行は、1列目にタイムスタンプ、2列目に選択したメニュー項目、3列目に読み取ったQRコード、としています。

Code.gs
// 選択したメニューの値も送信する
function onScan(text, menu) {
  SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(SHEET_NAME).appendRow([new Date(), menu, text]);
}

ここまでに追加したコードで、メニューで選んだ項目を読み取ったQRコードと一緒に記録できるようになりました。開発用のURLで正しく動作することを確認したら、再デプロイをして、本番用の動作を変更します。

メニュー項目をシートで管理する

ただ、表示するメニューを変更するたびに、再デプロイをするのは面倒です。そこで、スプレッドシートにメニュー項目一覧シートを追加し、シートを変更したらQRコードリーダーのメニューに反映するようにしてみましょう。

まず、スプレッドシートにMenuシートを追加して、メニュー項目を1行ごとに記述します。

image.png

そして、バックエンド側にメニュー一覧を取得するfetchMenus()を追加します。

Code.gs
const MENU_SHEET_NAME = 'Menu';

// メニュー一覧を取得する
function fetchMenus() {
  return SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(MENU_SHEET_NAME).getDataRange().getValues().map(row => row[0]);
} 

フロントエンド側で、このfetchMenus()を呼び出して、シートに記載されたメニュー項目一覧をHTMLにして表示します。Templated HTML(テンプレート化されたHTML)を使います。

フロントエンド側のコードは次のようになります。

index.html
   ...
    <div id="result"></div>

    <!-- メニューを表示。メニューを変更したら読み取り結果をリセットして二度読みを可能にする -->
    <select id="menu" onchange="CURRENT_TEXT = '';">
      <? for(let menu of fetchMenus()) { ?>
        <option><?= menu ?></option>
      <? } ?>
    </select>

    <script src="https://unpkg.com/html5-qrcode"></script>
    ...

また、Templated HTMLを使うために、バックエンド側のdoGet()を忘れずに変更します。

Code.gs
function doGet() {
  // HTMLの中で fetchMenus()を呼び出ため、テンプレートを使う
  const html = HtmlService.createTemplateFromFile('index').evaluate();
  html.addMetaTag('viewport', 'width=device-width, initial-scale=1');
  return html;
}

開発用のURLで正しく動作することを確認したら、再デプロイをして、本番用の動作を変更しましょう。

ソースコード

完成したスプレッドシートとApps Scriptはこちらです。どうぞコピーしてご自由にお使いください2。なお、Google Workspace(for Education)を使っている企業(学校)の方は、ブラウザーをシークレットモードにしてアクセスしてみてください。

フロントエンド

index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <div id="reader" style="width:340px;"></div>
    <button id="sound">Sound ON (iOS)</button>
    <div id="result"></div>

    <!-- メニューを表示ためにCode.gsのgetMenus()を呼び出す -->
    <!-- メニューを変更したら読み取り結果をリセットして二度読みを可能にする -->
    <select id="menu" onchange="CURRENT_TEXT = '';">
      <? for(let menu of fetchMenus()) { ?>
        <option><?= menu ?></option>
      <? } ?>
    </select>

    <script src="https://unpkg.com/html5-qrcode"></script>
    <script>
let CURRENT_TEXT = '';

const append = (text) => {
  document.querySelector('#result').innerHTML += `${text}<br>`; 
};

const onScanSuccess = (decodedText, decodedResult) => {
  console.log('onScanSuccess', decodedText, CURRENT_TEXT);
  if (CURRENT_TEXT !== decodedText) {
    CURRENT_TEXT = decodedText;
    append(decodedText);
    beep();
    // 選択したメニューを取得
    const menu = document.querySelector('#menu option:checked').innerText;
    google.script.run
      .withFailureHandler(function (error) {
        append(`"${decodedText}"の保存に失敗しました: ${error}`);
      })
      .onScan(decodedText, menu); // 選択したメニューをスプレッドシートに送信
  }
};

const html5QrcodeScanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: 240 });
html5QrcodeScanner.render(onScanSuccess);

// https://stackoverflow.com/questions/879152/how-do-i-make-javascript-beep
const DATA_URL = "data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=";  

const beep = () => {
  AUDIO.src = DATA_URL;
}

// for iOS audio
// https://stackoverflow.com/questions/31776548/why-cant-javascript-play-audio-files-on-iphone-safari
const AUDIO = new Audio();
AUDIO.autoplay = true;
document.querySelector("#sound").addEventListener("click", () => {
  beep();
});
    </script>
  </body>
</html>

バックエンド

Code.gs
const SHEET_NAME = 'Data';
const MENU_SHEET_NAME = 'Menu';

// 選択したメニューの値も送信する
function onScan(text, menu) {
  SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(SHEET_NAME).appendRow([new Date(), menu, text]);
}

// メニュー一覧を取得する
function fetchMenus() {
  return SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(MENU_SHEET_NAME).getDataRange().getValues().map(row => row[0]);
}

function doGet() {
  // HTMLの中で fetchMenus()を呼び出ため、テンプレートを使う
  const html = HtmlService.createTemplateFromFile('index').evaluate();
  html.addMetaTag('viewport', 'width=device-width, initial-scale=1');
  return html;
}

トラブルシューティング

Q. スプレッドシートをコピーして流用したいのですが、アクセスできないようです。

  • 所属組織のGoogle Workspaceの設定で、組織外のスプレッドシートへのアクセスが禁止されている場合があります。ブラウザーのシークレットモードだとアクセスできることがあります。お試しください。

Q. スクリプトを書き換えて、ブラウザーからアクセスし直しているのですが、書き換えた機能が反映されていないようです。

  • 開発用と本番用のURLがあります。開発用は最新のスクリプトが反映されていますが、本番用だと再デプロイが必要です。

Q. バックエンドからフロントエンドに値を渡すよう、スクリプトを書き換えてみました。Stringだけなら渡せるのですが、objectだとうまく渡せずnullになってしまいます。

  • google.script.runを使ったやり取りですが、バックエンドから渡せる値に制限があります。特にエラーが出るわけではないので気が付きにくいです。特にDate型のやりとりが制限されていることにご注意ください。

  • Date型をstring型に変換するのに、Utilities.formatDate()が便利です。

  1. QRコードは株式会社デンソーウェーブの登録商標です。

  2. ただし、著作権は放棄しません。また、このスクリプトによって発生したいかなる不利益についても責任を負うことはできません。 2

33
36
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
33
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?