WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

JavaScript の非同期関数

by WebSurfer 2022年11月23日 12:12

JavaScript の習得が遅々として進まない自分には、昔は無かった class とか => とか、さらには非同期関数とかが出てきて、Q&A サイトなどでの話題についていけない状況にあります。(汗)

というわけで、JavaScript の非同期についてちょっとだけ勉強してみました。以下に勉強したことを忘れないように書いておきます。

まず、非同期関数ですが、MDN のドキュメント「非同期関数」の説明が自分的には分かりやすいと思いました。

そのドキュメントによると "非同期関数は async キーワードで宣言され、その中で await キーワードを使うことができます" ということで、ドキュメントのサンプルコード(下に転載します)の asyncCall を非同期関数と呼ぶようです。

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: "resolved"
}

asyncCall();

非同期関数の利点は、"async および await キーワードを使用することで、プロミスベースの非同期の動作を、プロミスチェーンを明示的に構成する必要なく、よりすっきりとした方法で書くことができます" ということだそうです。

ということで、上で言う「プロミスベースの非同期の動作」すなわちコードに出てくる Promise, resolve が JavaScript における非同期関数の核心のようです。

まず、Promise というのは何かですが、自分的に分かりやすかったのは「現代の JavaScript チュートリアル」の Promise と「JavaScript Primer」の [ES2015] Promise という記事でした。(MDN のドキュメント「プロミスの使用」も読みましたがほとんど分かりませんでした)

記事に書いてあったことを要約して以下に書きます。(解釈が間違っているところもあるかも)

Promise は非同期処理の最終的な完了もしくは失敗を表すオブジェクトで、基本的に以下のように作成します。

let promise = new Promise((resolve, reject) => {
  // 処理
});

上の Promise コンストラクタの引数に渡している関数 (resolve, reject) => { // 処理 } は executor と呼ばれる関数で、resolve と reject という 2 つの引数を取ります。

resolve と reject は JavaScript に定義済みのコールバックで、以下のように executor の結果に応じていずれかを呼び出す必要があります。

  • resolve(value) – 正常に終了した場合。Promise の result プロパティを value に設定。
  • reject(error) – エラーが発生した場合。Promise の result プロパティを error に設定。

具体例は上のコードの resolveAfter2Seconds 関数を見てください。Promise オブジェクトを生成してそれを戻り値として返すようにしています。

asyncCall 関数が resolveAfter2Seconds 関数を呼ぶと、resolveAfter2Seconds 関数は Promise オブジェクトを生成して即座に戻り値として返します。

Promise コンストラクタの引数の executor 関数は new Promise の時点で自動的かつ即座に実行されます。

executor 関数内の setTimeout により 2 秒後に resolve('resolved') が呼ばれて、Promise オブジェクトの result プロパティは 'resolved' という文字列に設定されます。

asyncCall 関数内で resolveAfter2Seconds 関数に await が付与されているところに注目してください。await よって、resolveAfter2Seconds が戻り値として返した Promise が resolve されるまで次の行の console.log(result); には進まず待機します。

さらに await によって Promise オブジェクトの result プロパティのオブジェクトが取り出されて渡されます。上のコード例では resolve('resolved') としているので 'resolved' という文字列が渡されます。

そのあたりは C# の非同期処理で使う Task<T>, async, await と同様なようです。

実際に使ってみると理解が深まると思って、先の記事「canvas の画像をアップロード (その 2)」の JavaScript のコードに非同期関数を適用してみました。そのコードを載せておきます。

input type="file" でファイルの選択が完了すると発生する change イベントのリスナを非同期関数式にして、その中で Promise を使った関数 CreateDataUrl と CreateImage を await を付与して呼び出すようにしています。詳しくはコード内のコメントを見てください。

@{
    ViewData["Title"] = "Canvas2";
}

<h1>Canvas2</h1>

<input type="file" id="file1" />
<input id="button1" type="button" value="Upload" style="display:none;" />
<br />
<div id="msg"></div>
<canvas id="mycanvas"></canvas>
<div id="result"></div>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[

        const maxFileSize = 500000;
        const allowedContentType = "image/jpeg";

        const maxWidth = 500;
        const maxHeight = 500;

        let inputFile, uploadButton, msgDiv, myCanvas, resultDiv;

        // input type="file" から取得できる File オブジェクトを引数
        // に渡す。FileReader オブジェクトを生成して readAsDataURL 
        // メソッドで Data url を生成し、resolve コールバックの引数
        // に設定する 
        const CreateDataUrl = file => {
            return new Promise(resolve => {
                const reader = new FileReader();
                reader.addEventListener('loadend', 
                                e => resolve(e.target.result));
                reader.readAsDataURL(file);
            });
        };

        // Data url を引数に渡す。Image オブジェクトを生成しその 
        // src に Data url を代入。ロード完了後の Image オブジェ
        // クトを resolve コールバックの引数に設定する
        const CreateImage = dataUrl => {
            return new Promise(resolve => {
                const img = new Image();
                img.addEventListener('load', 
                                     e => resolve(e.target));
                img.src = dataUrl;
            });
        };

        // DOMContentLoaded イベントで初期設定を行う
        window.addEventListener('DOMContentLoaded', () => {

            inputFile = document.getElementById("file1");
            uploadButton = document.getElementById("button1");
            msgDiv = document.getElementById("msg");
            myCanvas = document.getElementById("mycanvas");
            resultDiv = document.getElementById("result");

            if (window.File && window.FileReader && window.FileList) {

                uploadButton.addEventListener('click', uploadImage);

                // リスナを非同期関数式にして、リスナの中で Promise 
                // を使った関数 CreateDataUrl と CreateImage を 
                // await を付与して呼び出す。
                inputFile.addEventListener('change', async () => {
                    resultDiv.innerText = "";

                    if (ClientValidate(inputFile) == false) {
                        uploadButton.style.display = "none";
                        myCanvas.style.display = "none";
                        return;
                    }

                    //  選択された画像の Data url を取得
                    let dataUrl = 
                        await CreateDataUrl(inputFile.files[0]);

                    // 画像をロードした Image オブジェクトを取得
                    let img = await CreateImage(dataUrl);

                    // canvas に画像を描画
                    DrawImageOnCanvas(img);
                });
            }
            else {
                inputFile.style.display = "none";
                myCanvas.style.display = "none";
                msgDiv.innerText =
                    "File API がサポートされていません。";
            }
        });

        // 選択されたファイルの検証のためのヘルパ関数
        function ClientValidate(fileUpload) {
            msgDiv.innerText = "";

            if (fileUpload.files[0] == null) {
                msgDiv.innerText = "ファイルが未選択です。";
                return false;
            }

            if (fileUpload.files[0].type != allowedContentType) {
                msgDiv.innerText = "選択されたファイルのタイプが " +
                    allowedContentType + " ではありません。";
                return false;
            }

            if (fileUpload.files[0].size > maxFileSize) {
                msgDiv.innerText = "ファイルのサイズが " +
                    maxFileSize + " バイトを超えています。";
                return false;
            }

            return true;
        }

        // 画像が読み込み済みの Image オブジェクトを引数に受け取る。
        // その画像を指定されたサイズに縮小して canvas に描画
        function DrawImageOnCanvas(image) {
            // オリジナル画像のサイズ
            const w = image.width;
            const h = image.height;

            let targetW, targetH;
            const context = myCanvas.getContext('2d');

            if (w <= maxWidth && h <= maxHeight) {
                // w, h ともに制限 maxWidth, maxHeight 以内 ⇒
                // そのままのサイズで canvas に描画
                myCanvas.setAttribute('width', w);
                myCanvas.setAttribute('height', h);
                context.drawImage(image, 0, 0);
            }
            else if (w < h) {
                // w, h どちらかが制限オーバーで h の方が大きい ⇒
                // 高さを maxHeight に縮小
                targetH = maxHeight;
                // 幅は高さの縮小比率で縮小
                targetW = Math.floor(w * targetH / h);
                myCanvas.setAttribute('width', targetW);
                myCanvas.setAttribute('height', targetH);
                context.drawImage(image, 0, 0, targetW, targetH);
            }
            else {
                // w, h どちらかが制限オーバーで w の方が大きい ⇒
                // 幅を maxWidth に縮小
                targetW = maxWidth;
                // 高さは幅の縮小比率で縮小
                targetH = Math.floor(h * targetW / w);
                myCanvas.setAttribute('width', targetW);
                myCanvas.setAttribute('height', targetH);
                context.drawImage(image, 0, 0, targetW, targetH);
            }

            uploadButton.style.display = "inline-block";
            myCanvas.style.display = "block";
        }

        // canvas の画像データを DataURL 形式で取得し、JSON 文
        // 字列を組み立てて fetch API でサーバーに送信する
        async function uploadImage() {
            const context = myCanvas.getContext('2d');
            const dataUrl = context.canvas.toDataURL("image/jpeg");
            const params = {
                method: "POST",
                body: '{"imgBase64":"' + dataUrl + '"}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch("/api/Canvas", params);
            if (response.ok) {
                const message = await response.text();
                resultDiv.innerText = message
            } else {
                resultDiv.innerText = "アップロード失敗";
            }
        }

        //]]>
    </script>
}

非同期関数がある程度理解できれば、先の記事「canvas の画像をアップロード (その 2)」のコードに比べてかなり読みやすくなったような気はします。

Tags: , , , , ,

JavaScript

JavaScript で Html Escape

by WebSurfer 2021年5月18日 11:25

ユーザー入力文字列のサニタイジング(無害化)のためのエスケープ処理を JavaScript で行うためのコードを備忘録として書いておきます。以下のコードは ES6 をサポートしてない IE11 でも動くことは確認しました。

<script type="text/javascript">
    //<![CDATA[

    function htmlEscape(str) {
        if (!str) return;
        return str.replace(/[<>&"'`]/g, function (match) {
            const escape = {
                '<': '&lt;',
                '>': '&gt;',
                '&': '&amp;',
                '"': '&quot;',
                "'": '&#39;',
                '`': '&#x60;'
            };
            return escape[match];
        });
    }

    // 以下は検証用
    var str = '& < > ` " ' + "'";
    console.log(htmlEscape(str));

    // 結果 (IE11 も同様):
    // &amp; &lt; &gt; &#x60; &quot; &#39;

    //]]>
</script>

そんな情報はググればすぐ出てくると言われるかもしれませんが、基本的にネットの記事は自分で検証して確認するまで信用できない疑り深い性格なものですから。(笑)

クロスサイトスクリプティング(XSS)対策の一つに、ユーザー入力文字列のサニタイジング(無害化)がありますが、その目的で上記コードは文字列内の < > & " ' ` の 6 文字を &lt; &gt; &amp; &quot; &#39; &#x60; にエスケープ(変換)しています。

ASP.NET MVC などではフレームワーク組み込みでサニタイジング機能が含まれていますので、Html ヘルパーや Tag ヘルパーを使っている限り上のスクリプトの使い道はないです。

では、どういうときに使うかと言うと、例えば以下のように、Web API などから取得した文字列で html ソースを組み立てる場合でしょうか。

function getCars() {
  $.ajax({
    type: "POST", 
    url: "Api/GetCarsByDoors", 
    data: '{"doors":' + $("#ddlDoors").val() + '}', 
    contentType: "application/json; charset=utf-8",
 
    success: function (cars) {      
      $('#output').empty();
      $.each(cars, function (index, car) {
        $('#output').append(
          '<p><strong>' + car.Make + ' ' +
          car.Model + '</strong><br />Description: ' +
          htmlEscape(car.Description) + '</p>');
        });
      }
  });
}

Tags: , ,

JavaScript

Canvas に複数の画像を順番通り描画

by WebSurfer 2019年10月24日 12:30

Canvas に複数の画像を指定した順序で描画し、その後 Canvas に描画された画像を Data Url 形式で取得する方法を書きます。元の話は Teratail のスレッド「CanvasにdrawImageで描写した画像をtoDataURLで取得したい」です。

Canvas と img タグの画像

上の画像がその結果を Chrome で表示したもので、左が Canvas の画像、右が canvas.toDataURL メソッドを使って Data Url 形式で取得した画像データを img 要素の src 属性に設定して表示した結果です。

Canvas に表示する複数の画像の数だけ new Image() で img 要素を生成し、その src 属性に画像の url を設定すると img 要素が画像に読み込みを完了した時点で load イベントが発生するので、それにリスナをアタッチして Canvas への書き込みと Data Url 取得の処理を行うのが基本です。

ただし、問題は、Canvas に描く画像が複数ある場合、img 要素による画像の読み込みにかかる時間が画像のサイズなどによって異なるので、複数ある img 要素の load イベント発生の順番は不定となることです。

もう一つ、canvas.toDataURL メソッドを使って Canvas から Data Url 形式のデータを取得するタイミングも問題です。全ての画像の Canvas への描画が終わるのを待たなければなりません。

上の問題に対応するには、最後の load イベントの発生を待って、そのイベントリスナで複数ある全ての img 要素の画像 を希望する順番に context.drawImage メソッドで Canvas に書き込むのがよさそうです。

context.drawImage は同期メソッドなので、同じリスナ内で、全ての画像の context.drawImage が終わった後に canvas.toDataURL メソッドを記述すれば Data Url 形式のデータを取得できます。(リスナの外に canvas.toDataURL メソッドを置くのはダメです。img 要素の load イベントが発生する前に canvas.toDataURL に制御が飛ぶので)

検証に使ったコード、即ち上の画像を表示したコードを以下に書いておきます。(ASP.NET のページを利用していますが、そこは本題とは関係ないので気にしないでください) 元の画像は 125px x 75px の色が異なる .png 画像 5 枚です。それらを 20px づつ右下にずらしながら描画しています。

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeFile="0076Canvas2.aspx.cs" Inherits="_0076Canvas2" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; 
  charset=utf-8"/>
  <title>0076Canvas2.aspx</title>
  <script src="/Scripts/jquery.js"></script>
  <script type="text/javascript">
  //<![CDATA[
    window.onload = function () {
      // それぞれ 125px x 75px の単色 png 画像
      // 色は名前の通り赤, 青, 緑, 黄, 橙
      var srcs = [
          "/Images/red.png",
          "/Images/blue.png",
          "/Images/green.png",
          "/Images/yellow.png",
          "/Images/orange.png"
      ];

      var canvas = document.getElementById('mycanvas');
      var context = canvas.getContext('2d');

      // canvas に描かれる順番を保証するには以下のようにする。
      // そうすればリスナの中に canvas.toDataURL("image/png");
      // を記述でき Data Url が取得できる
      var images = [];
      var loadCount = 0;
      for (let i = 0; i < srcs.length; i++)
      {
        images[i] = new Image();                
        images[i].onload = function () {
          loadCount++;
          if (loadCount == images.length)
          {
            for (let j = 0; j < images.length; j++)
            {
              context.drawImage(images[j], j * 20, j * 20);
            }

            // image.onload のリスナの中でないと Data Url
            // は取得できないので注意
            var result = canvas.toDataURL("image/png");
            document.getElementById("image1").src = result;
          }
        };
        images[i].src = srcs[i];
      }
    }
    //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
  <div style="float: right;">
    <canvas id="mycanvas" width="205" height="155">
    </canvas>
  </div>
  <div>
    <img id="image1" alt="" />
  </div>
  </form>
</body>
</html>

他に jQuery.Deferred() を使う方法もあります。そのコードを以下に書いておきます。

// jQuery.Deferred を使用
var images = [];
var dfds = [];
for (let i = 0; i < srcs.length; i++)
{
    images[i] = new Image();
    let dfd = new $.Deferred();
    dfds[i] = dfd;
    images[i].onload = function () { dfd.resolve(); };
    images[i].src = srcs[i];
}

$.when.apply($, dfds).done(function () {
    for (let i = 0; i < images.length; i++)
    {
        context.drawImage(images[i], i * 20, i * 20);
    }
    var result = canvas.toDataURL("image/png");
    document.getElementById("image1").src = result;
});

全ての img 要素の load イベントが発生し終わるのを待って Canvas への描画と Data Url 形式のデータを取得するというところは同じですが、見かけは何となくカッコいいかも。(笑)

Tags: , , , ,

JavaScript

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年5月  >>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar