echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Serverless Framework】簡単デプロイとserverless.ymlの記載について その2

※2019/7/1 デプロイ先バケット名を任意の名前にする方法を追記しました。

概要

前回Serverless Frameworkについて書きましたが、書ききれなかった点があったので、今回はその点についての内容になります。

serverless.ymlで使用できる変数について

Serverless.ymlでは、下記の変数を使用できます。

  • 変数は${self;;xxx}のように、「${}」形式で参照します。
  • 「custom」項目内に変数名と値のペアを設定することで、独自の変数&値を設定することができます。
  • 外部ファイルに定義した変数を使用することもできます。(下記ソース参照)
  • 下記変数は、カンマの後に値を指定することで、初期値を設定可能。
記載方法 説明 備考
${self:xxx} そのserverless.ymlファイル自身の項目(xxx)を参照する
${env:YYY} 使用しているPCの環境変数(YYY)を参照する
${opt:zzz} deployコマンド実施時に、引数(zzz)に指定された値を参照する
${file(aaa):bbb} 外部ファイル(aaa)に定義した変数(bbb)を参照する bbbは外部ファイルの変数名。省略した場合、外部ファイルの変数すべてが読み込まれる。
${cf:リージョン名.ccc} 別のCloudFormationの「出力」cccに設定されている値を参照する 「出力」にはServerless.ymlの「Outputs」で登録可能。
リージョン名は省略可能。その場合、デフォルトのリージョン名が適用される。
service:
  name: makky12-service
  
# 独自の変数を定義する場合、こんな感じでcustom内に定義する。
custom:
  author: makky12

  # 外部ファイルの変数を参照する例。  
  # exportは外部ファイルの内容をすべて読み込み、export_partnerは外部ファイルの  
  # 特定の変数(partner)の値のみを読み込む。  
  # またどちらも実際に読み込むファイルは、ルート直下の「--env xxx」で  
  # 指定されたxxxフォルダ内のserverless.ymlファイル。  
  # 例えば、 deploy --env dev なら、ルート直下の「dev」フォルダ内の  
  # serverless.ymlファイルを参照する。
  export: ${(./${opt:env, 'dev'}/serverless.yml)}  
  export_partner: ${(./${opt:env, 'dev'}/serverless.yml):partner}  
  
provider:  
  # 環境変数を使用する例。  
  # 例えば事前にser MY_REGION=ap-northeast-1としていた場合、  
  # regionにはap-northeast-1が設定される。  
  region: ${env:MY_REGION}  
  
  apiName: HogehogeGateway  
  
  # 例えば、事前にOutputsに下記定義をしたserverless.ymlをデプロイした場合...
  # resources:
  #   Outputs:
  #     Value:
  #       "Ref": myRestApi
  #     Export:
  #       name: myRestApiId  
  #  
  # restApiIdには、スタック名が「myStack」というCloudFormationの、  
  # 事前にmyRestApiで定義したAPI GatewayのRestAPI IDが代入される。
  apiGateway:  
    restApiId: ${cf:myStack.myRestApiId}  
  
# serverless.ymlの変数を使用する方法
functions:
  myFunction:  
    # この場合、参照先serverless.ymlで下記定義をしていた場合、  
    # partner: abc  
    # thanksto: xyz
    #   
    # 実際のdescriptionの出力内容は以下の通りになる。  
    # author=makky12_partner=abc_thanksto=xyz
    description: author=${self:custom.author}_partner=${self:custom.export_partner}_thanksto=${self:custom.export.thanksto}
  myFunction2:
    # もちろん、serverless framework既定の項目の値を参照することも可能。
    description: ApiGateway=${self.provider.apiName}

deploymentBucket記載時の注意

前回「deploymentBucketには、パッケージ化したファイルが格納されるバケット情報を指定する」と記載しましたが、これには一つ問題があり、

name(バケット名)を定義すると、その名前のバケットが存在しない場合、エラーになる

という現象があります。(nameを指定しない場合、バケットがデフォルトの名前で新規作成されます。またnameのバケットが既に存在する場合は問題ありません)

これだとバケット名を任意の名前で新規作成できないのですが、下記方法で回避できます。

  • npm経由で「serverless-deployment-bucket」モジュールをインストールする
  • serverless.ymlの「plugins」に、このモジュールを追加する。(配列で定義する)
  • deploymentBucket.nameに、デプロイ先バケットの名前を設定する。
  • serverless-deployment-bucketの設定を「custom」に定義する。(下表参照。なおすべて任意項目で、値はすべてbooleanで指定。)
項目名 説明 初期値
versioning デプロイ先バケットで、バージョニングを可能にする false
enabled serverless-deployment-bucketを有効にするか true
policy バケットのポリシーをJSONで出力するか(?) なし(設定しない)
> npm install serverless-deployment-bucket --save-dev
# オプション項目を設定する場合。(基本、デフォルトで問題ないと思う)
custom:  
  enabled: true  
  versioning : true  
  policy: true  
    
plugins:
  - serverless-deployment-bucket  
  
provider:  
  deploymentBucket:  
    name: self-custom-bucket-name  

【参考】:serverless-deployment-bucket

デプロイのやり方

といっても、デプロイ自体はとても簡単で、サービスのルートフォルダ(serverless.ymlがあるフォルダ)で、「sls deploy」コマンドを実施するだけです。
ただ、いくつか便利なオプションがあります。

オプション名 説明 備考
--packege(-P) <格納先フォルダのパス> 関連ファイルのパッケージのみ行い、デプロイはしない フォルダのパスを省略した場合、serverlessというフォルダを作成し、そこにパッケージ化したファイルを格納する
--verbose(-v) デプロイ状況をコンソール上で確認できる CloudFormationのログに近い。よく使うと思う。
--function(-f) <functions:内の定義名> 定義名のLambda関数のみデプロイし、全体のデプロイは行わない 一回デプロイ後、Lambda関数のみ修正した場合に便利。通常デプロイに比べて、短時間で済む

もちろん、まだまだフォローしきれてない部分もありますので、そこは適宜公式サイトで確認すると、いろいろ理解が深まると思います。

それでは、今回はこの辺で。

【Serverless Framework】簡単デプロイとserverless.ymlの記載について その1

※2019/6/27 Outputs項目に関する記載を追加しました。
※2019/6/27 さらにapiKeys/usagePlan/private項目に関する記載を追加しました。

概要

近日、仕事でAWSを触っていますが、その関係でServerless Frameworkを触ることになりました。

公式サイト:Serverless Framework

Serverless FrameworkはSAM同様、サーバーレスアプリ開発フレームワーク(正確にはテンプレートモデルに近い。Webアプリなどのフレームワークとはちょっと違う)CLIで、以下のことを行うことができます。

  • 環境(言語)ごとのテンプレートモデルの適用
  • 使用するリソース(Lambda、S3、DymnamoDBなど)の定義
  • コマンドライン上からの一括デプロイ(CloudFormationへの展開)

また、主に下記の点がSAMと異なります。

  • マルチクラウドプラットフォームである。(AWSだけでなく、AzureやGCPなどでも使える)
  • (ローカル環境での)デバッグ実行に、コンテナ(Dockerなど)が不要

があります。

ただ、公式ドキュメントが全編英語で、解説してるサイトもあまり多くなかったので、自分なりに「とりあえずデプロイ」できるところまでをまとめました。

インストール &プロジェクト作成

インストールは公式サイトのトップにもある通り、npm installをするだけです。(「-g」は必要に応じて置き換えてください。)

npm install -g serverless

プロジェクト作成は「serverless create」コマンドで作成します。

  • テンプレート名は言語別のテンプレート名。「aws-nodejs」「aws-python3」「aws-csharp」など。(詳細は公式ページを参照)
  • サービス名はアプリのプロジェクト名(≒アプリ名)。Serverless Frameworkでは「サービス」という扱いになる。
  • プロジェクトフォルダのパスはオプション。このフォルダがない場合、プロジェクト作成時に自動で作成して、そこをプロジェクトのルートにしてくれる。
# 「sls」は「serverless」の省略系。どちらでも実行可。
sls create --template (テンプレート名) --name (プロジェクト名)  --path (プロジェクトフォルダのパス)  
# e.g.  
sls create --template aws-nodejs --name makky12-serverless-fw-app --path makky12-dir

テンプレートファイル(serverless.yml) の定義

※これ以降は、AWSをベースに説明します。(それ以外の場合は、公式ドキュメントを参照)

serverless.ymlは、デプロイされるサービスの定義を行うファイルです。
(SAMのtemplate.ymlに該当)

つまりこの内容が、CloudFormationやLambdaなどの各種機能に設定されます。

ただこの公式ドキュメントのボリュームが大きいので、補足説明がいるなという項目だけ説明します。
(基本的なことは、公式の下記ページに書いてあります。全編英語ですが)

※プロジェクトに最初から入ってるhandler.jsを動かすだけなら、下記を設定してデプロイを実施するだけで、API Gateway経由でブラウザ実行できます。
(項目数は多いですが、ほとんどにデフォルト値が設定されてます。)

  • providerに「region: ap-northeast-1」を設定する。
  • 66 ~ 69行目(「events」~「method:get」)までのコメントを外す

ルート項目(インデントがない項目)

項目名 説明 備考
provider 複数項目で共通で使用する項目の値を設定する 項目別に上書き可能
custom serverless.yml内で使用する変数と値のペアを設定する 変数については後述
package プロジェクトをパッケージする際の設定をする
plugins 外部プラグインを使用する場合、プラグイン名を記入する 公式に書いてないんだけど...
functions Lambda関数の設定をする
resources プロジェクトで使用するリソース(S3/DynamoDBなど)の設定をする

provider項目

項目名 説明 備考
stage dev(開発)/stg(ステージング)など、環境が分かる設定をする 初期値はdev
〇〇name CloudFormation/API Gatewayなどで使用される名前 未指定の場合、デフォルト名がつけられる(プロジェクト名+αなど)
apiKeys 作成するAPIKeyの名前を指定する 配列で指定する
usagePlan 使用量プランの定義を指定する apiKeysに指定したAPIKeyに紐づく使用量プランとして登録される。
(なぜか)usagePlanName(使用量プラン名)は指定できない。(resourcesだと指定可能)
profile credentialを定義済の場合、そのプロファイル名を指定する デプロイの際、設定したプロファイルの認証情報を使用する
deploymentBucket パッケージされたファイルを格納するS3のバケット名を指定する 注意が必要。(後述)
role 全Lambda関数に適用される共通のIAMロールを設定する
iamRoleStatements Lambda関数以外のリソース(S3/DynamoDBなど)のIAMロールの設定をする
environment Lambda関数の環境変数と値のペアを設定する

package項目

項目名 説明 備考
include/exclude パッケージされるファイルとして含めたい/除きたいファイルを指定 ワイルドカードによる再帰的指定可能。tsconfig.jsonの同盟項目と同じ
excludeDevDependencies excludeDevDependenciesのモジュールを自動で除外するか デフォルトはtrue
artifact 自分でパッケージをする際のパッケージファイルの置き場? include/exclude設定関係なく、deploy時にパッケージファイルから除外される
individually Lambda関数単位でのパッケージを行うかどうか

functions項目

※functionsの子要素には、デプロイされるLambda関数のfunction名を記載する。下記はその子要素(functionsの孫要素)

項目名 説明 備考
handler 実行する関数。[ファイル名.exports名]形式で記載
events Lambda関数を実行するトリガーとなるイベントの種類 http(API Gateway)/s3/Alexa Skillなど
reservedConcurrency この関数を同時並行で実行できる数?
environment この関数に定義する環境変数と値のペア

resources項目

※resources要素には「Resources」-「リソース定義名」までは決め打ちになる。(「serverless.ymlのサンプル」参照)
下記は「リソース定義名」の子要素になる。(=resourcesのひ孫要素)
※Propertiesの子要素は、Typeごとに異なる。CloudFormationのテンプレートを参照。

項目名 説明 備考
Type リソースのタイプ。DynamoDB/S3など [ファイル名.exports名]形式で記載 AWS::S3::Bucketのように、CloudFormation形式で記載
Properties リソースタイプ別の詳細設定 S3ならバケット名など。(設定項目はリソースにより異なる)

Outputs項目

※Outputsの子要素には、Output項目の定義名を指定する。下記はその子要素になる。(=Outputsの孫要素)
※Outputsに定義した項目はCloudformationの「出力」に登録され、別のCloudFormationテンプレイートファイルから参照することができる。
※もちろん別のserverless.ymlファイルからも参照することが可能。(詳細はその2で記載予定)

項目名 説明 備考
Value 他の定義で参照させたいリソース、プロパティなどの情報。 Ref, Fn::GetAttなど、参照を使用して設定することが可能
Export 他の定義から参照する際に使用する名前(≒エイリアス) 「Name」プロパティに設定する
Description この定義についての説明

serverless.ymlのサンプル

app: makky12-serverless-app

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
frameworkVersion: ">=1.0.0"

custom:  
  author : makky12  

provider:
  name: aws
  runtime: nodejs10.x  
  stage: dev
  region: ap-northeast-1
  stackName: makky12-serverless-app
  apiName: makky12-serverless-app-dev
  deploymentBucket:
    name: makky12-serverless-app-dev-bucket
    serverSideEncryption: AES256
  deploymentPrefix: makky12
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "s3:ListBucket"
      Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
    - Effect: "Allow"
      Action:
        - "s3:PutObject"
      Resource:
        Fn::Join:
          - ""
          - - "arn:aws:s3:::"
            - "Ref" : "ServerlessDeploymentBucket"
            - "/*"

package:
  exclude:
    - exclude-dir/readmme.txt

functions:
  greeting:
    handler: handler.greeting
    events:
      # httpはAPI Gatewayがトリガになるイベント。  
      # corsはクロスオリジン制約に関する設定。
      # privateにtrueを設定すると、APIキーの指定が必須になる。(x-api-key)
      - http:
          path: users/create
          method: get
          cors: true
          private: true

  environment:
    NENGO: Reiwa

resources:
  Resources:
    makky12Bucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: makky12-bucket
    makky12Table:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: makky12Table
        AttributeDefinitions:
          - AttributeName: e-mail
            AttributeType: S
        KeySchema:
          - AttributeName: e-mail
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

Outputs:
  MyWebHookUrl:
    Description: The Url of the Custom WebHook
    Value:
      "Fn::GetAtt": [ myWebHook, Url ]
    Export:
      Name: myWebHookUrlVal

と、なんか書いてたらものすごく長くなってしまいましたので、本当はデプロイやserverless.ymlの変数仕様についても書く予定だったのですが、それはその2に書きます。

【Microsoft】de:code2019体験記その2 面白かったセッション紹介

概要

前回、de:code2019の「C# ドキドキ・ライブコーディング対決」を自分でも実施しましたが、今回は純粋に面白かったセッションの紹介をしようと思います。

Xamarin.Forms アプリケーション設計パターン

マイクロソフトの大田 一希氏(かずきさん)のセッション。
Xamarin.Formsの新機能や、設計&作成時のパターンなどについて、詳しく解説していただきました。

また、デモもあり、動作しているところを実演していただける...
はずだったんですよね。

マイクロソフトのこういう大きいイベントでは、どなたか一名は必ず本番で悲劇に見舞われるのですが...今回はかずきさんだったようです。(意味深)

とはいえ、楽しませて頂きましたし、僕もその気持ちは痛いほどわかりますからね。(てか何で、起こって欲しくないときに限って、確実に起こるんでしょうね、フリーズって。)

Windows 10 対応のデスクトップアプリを作る技術

上に続き、再びかずきさんのセッション。
UWPとかWPFとか、その他についての情報でした。

てか、UWPって敬遠気味だったんですが、すごい進化してるんですね。
デモを見てて、すごくおもしろそうでした。

何事も、食わず嫌いはいけませんね。ホントに

エンジニアの人生設計 ~どのようにキャリアを描いていけばよいのか~

MSのテクノロジーセンター長(要はめちゃくちゃ偉い人)、澤円さんのセッション。
僕がde:code2019で一番聞きたかったのがこのセッション。

いや、事前に一番人気と聞いていましたが(しかも澤さん本人から!)、想像以上でした。 (さすがに参加者が多かったとはいえ、前のセッション開始時から行列ができてたのは、このセッションだけです)

しまいには部屋に収まりきらず、急遽別会場を用意して、それでも収まりきらない...という大盛況ぶり。

さすが「プレゼンの神」の異名は伊達じゃないなあ、と思いました。

さて内容は、技術とは一切関係はないのですが、いわば「人生を豊かに」そして「幸せに生きるには」という、非常に重要なテーマです。

そして重要なのは、

  • 自己分析&自分に合ったことをする
  • キャリアの定期的な見直し&ポートフォリオの公開
  • ビジネスのつながりを積極的に構築する(Twitter&LinkedInなどで)
  • 睡眠だけは、絶対おろそかにしてはダメ!

という、分かっているんだけど、なかなか出来ないことの重要性を改めて教えてくれる、ありがたい内容でした。

...僕も幸せになりたいなあ。

結論

個人的は、やはり勉強会に限らず、たとえ東京の勉強会・イベントであっても、出来る限りこういうイベントは参加しておくべきだなあ、と感じました。

確かに出費は痛いんですが、色々な人と出会えたり、新しい技術を知ることが出来るのは大きいですし、何よりモチベーションの上がり方が半端ないです。

来年も、ぜひde:codeに参加したいなあ、と思いました。

...てなことを、現在(2019/6/13 21時ちょうど)AWS Summitに向かう新幹線の中で書いてます。

【Microsoft】de:code2019体験記その1 C#で令和元年を表示する

概要

先日、マイクロソフトのde:code2019に参加してきました。

本当に素晴らしいセッションを通して勉強することが多く、行って正解だったと思います。
それにこういうイベントで知見が広まると、モチベーションも上がりますね。

あと、某スペシャルゲストも、本当にスペシャルな人でしたからね。

そんな中で「C# ドキドキ・ライブコーディング対決 @ de:code - ONLY C#!! Blazor Web 開発バトル」というセッションがありました。

私は残念ながら拝聴できなかったのですが、「その場で与えられたお題に対して、即興で時間内にプログラミングして動かす」というものです。
ちなみにお題は「C#で令和元年を表示する」だったようです。

「じゃあ自分でやったら、何分かかる?」ということで、やってみました。

結果

とりあえず僕が思いついたのは、下記のコード。
(WinFormアプリとして動くのは確認済です。)

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Windows.Forms;
  
namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
  
        private void button1_Click(object sender, EventArgs e)
        {
            var Ymd = textBox1.Text;
            var listNengoPeriod = new List<NengoPeriod>();
  
            listNengoPeriod.Add(new NengoPeriod(new DateTime(2019, 5, 1), "令和"));
            listNengoPeriod.Add(new NengoPeriod(new DateTime(1989, 1, 7), "平成"));
            listNengoPeriod.Add(new NengoPeriod(new DateTime(1926, 12, 26), "昭和"));
            listNengoPeriod.Add(new NengoPeriod(new DateTime(1912, 7, 30), "大正"));
            listNengoPeriod.Add(new NengoPeriod(new DateTime(1868, 1, 25), "明治"));
  
            var resultNengo = String.Empty;
  
            if (DateTime.TryParse(Ymd, out var dt))
            {
                var nengo = (from n in listNengoPeriod
                             where n.getPeriod <= dt
                             orderby n.getPeriod descending
                             select n).FirstOrDefault<NengoPeriod>();
  
                resultNengo = nengo == null ? "明治より前" : nengo.getNengo;
            }
  
            var message = string.IsNullOrEmpty(resultNengo) ? "指定した年月日が不正です。" : $"{Ymd} は、{resultNengo} です。";
            MessageBox.Show(message);
        }
    }
  
    class NengoPeriod
    {
        public NengoPeriod(DateTime period, string nengo)
        {
            this.getPeriod = period;
            this.getNengo = nengo;
        }
  
        public DateTime getPeriod { get; }
        public string getNengo { get; }
    }
}

上記コード完成にかかった時間は、30分でした。

なお、実際の登壇者がコード完成までにかかった時間は、たったの5分とのこと。
(細かいバグはあったようですが)

いや、カップ麺の待ち時間程度でこれを完成させちゃうって、本当すごいな~、と思いました。
(しかも実際は、トークをしながらとかですからね。)

やはりすごい人はいるんだなあ、と改めていい刺激になりました。

てか、対峙する価値のない人なんて適当にスルーして、その分こういう方々とご一緒させて頂ける時間に割くべきですね、本当に。

※体験記その2は、また後日。

【Javascript】非同期処理の待ち時間にアニメーションを表示する

概要

Webやスマホアプリで、何か非同期処理を行っている際に画面に表示される、くるくる回ったりするアニメーション(正式にはなんていうの...Xamarin.Formsの「Activity Indicator」です)を表示する方法です。

f:id:Makky12:20190517200801g:plain

作成方法

まず「画像はどうするの」となりますが、これは「Loader Generator」というサイトで、簡単に作成&ダウンロードできます。
※サイズ・回転速度・色など、いろいろ細かいところまで調整でき、便利です。

実際に表示する方法

これは、実際にサンプルを見た方が早いと思いますので、ソースを。
(ソース自体は、そこまで難しい内容ではないので、説明は省略)

/* ------------------------------
 非同期処理を実行する関数
 ------------------------------ */
function asyncFunc() {  

    // Loading 画像を表示
    dispLoading("処理中...");
   
    // 非同期処理(例)
    $.ajax({
      async : true,
      url : "http://xxx.com/yyy/zzz",
      type:"GET",
      dataType:"json"
    })
    // 通信成功時
    .done(function(data) {
      alert("成功しました");
    })
    // 通信失敗時
    .fail( function(data) {
      alert("失敗しました");
    })
    // 処理終了時
    .always( function(data) {
      // Loading 画像を消す
      removeLoading();
    });  
}  
    
/* ------------------------------
 表示用の関数
 ------------------------------ */
function dispLoading(msg){
  // 引数なしの場合、メッセージは非表示。
  if(msg === undefined ) msg = "";
  
  // 画面表示メッセージを埋め込み
  var innerMsg = "<div id='innerMsg'>" + msg + "</div>";  
  
  // ローディング画像が非表示かどうかチェックし、非表示の場合のみ出力。
  if($("#nowLoading").length == 0){
    $("body").append("<div id='nowLoading'>" + innerMsg + "</div>");
  }
}
 
/* ------------------------------
 表示ストップ用の関数
 ------------------------------ */
function removeLoading(){
  $("#nowLoading").remove();
}  
#nowLoading {
  display: table;
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  background-color: #fff;
  opacity: 0.8;
}
 
#innerMsg {
  display: table-cell;
  text-align: center;
  vertical-align: middle;
  padding-top: 140px;
  z-index:100;
  background: url("表示するくるくる画像のURL]") center center no-repeat;
}

ポイント

  • 非同期処理開始したら「#NowLoading」及び「#innerMsg」のタグを埋め込み、終了したら削除する。
  • 「#NowLoading」のタグは、body直下の子要素にする。
  • z-indexは、モーダルなどを表示する場合は、大きい値にしておく。
    • モーダルのz-indexとの兼ね合いで非表示になることがある。
  • 「#NowLoading」のタグは、もちろんHTMLに記載してもOK。
    • その場合、visibility:visible(hidden)などで制御する。

と、また簡単になりましたが、今回はこの辺で。

【Javascript】非同期処理のPromiseやasync/awaitについて その2

前回はPromiseについて書きましたが、その続きで今回はasync/awaitについて。

【参考】
async/await 入門(JavaScript)

asyncとは

非同期関数を定義する関数宣言のこと。
async関数は、以下の動作をする。

  • Promiseを返す
  • 値をreturnすると、Promiseはそれをresolveする。
  • エラーなどをスローすると、Promiseはそれをrejectする。
  • 関数内部でawaitを使用できる。
// 普通こんな書き方しませんが、サンプルということで。  
function main() {  
    
    const even = checkEven(2);
  
    // この場合、コンソールには「偶数です」と表示される。
    even.then((message) => {
        console.log(message);
    }).catch((err) => {
        console.log(err.message);
    });
  
    const odd = checkEven(1);
  
    // この場合、コンソールには「奇数です!」と表示される。
    odd.then((message) => {
        console.log('偶数です');
    }).catch((err) => {
        console.log(err.message);
    });
}
  
async function checkEven(num) {
    if(num % 2 === 0) {
        // resolve('偶数です')と同じ。
        return '偶数です';
    } else {
        // reject('奇数です!')と同じ。
        throw new Error('奇数です!');
    }
}

awaitとは

  • Promiseを返す関数について、Promiseの結果(resolveやreject)が返されるまで、処理を待つ。
    • 同期処理みたいな動作になる。
  • awaitを付けた場合、戻り値に格納されるのはresolve、あるいはrejectされた値となる。
  • rejectされたのがエラー場合、そのエラーが発生する。(catch()処理などを実施する必要あり)
  • async関数内でのみ使用できる。

先程の関数を、awaitを使って書くと、こうなります。

async function main() {  
    
    // この場合、messageにはresolve値の「偶数です」が格納されるので、  
    // それがそのまま表示される。
    try {
        const even = await checkEven(2);
        console.log(even);
    } catch(err) {
        console.log(err.message);
    }
  
    // この場合、checkEvenからErrorがスローされるので、コンソールには  
    // err.message(「奇数です!」)が表示される。
    try {
        const odd = await checkEven(1);
        console.log(odd);
    } catch(err) {
        console.log(err.message);
    }
}
  
async function checkEven(num) {
    if(num % 2 === 0) {
        // resolve('偶数です')と同じ。
        return '偶数です';
    } else {
        // reject('奇数です!')と同じ。
        throw new Error('奇数です!');
    }
}

await使用時の注意点

  • awaitは基本的に「resolveの値がないと、それ以降の処理ができない」際に使用する。(下記など)
    • ファイルの内容を取得する
    • DBのあるテーブルのレコードを取得する
  • awaitは先述の通り「Promiseの結果が返されるまで、処理を待つ」動作。
    • async関数だからと言って、なんでもawaitすればいいわけではない。
    • 使い方を間違えると、レスポンス時間の遅延につながる。(下記ソース)
async function main() {  
    
    // funcVal1~funcVal3が「それぞれ処理に5秒かかる」場合、  
    // awaitで直列処理してしまうと、main()が終わるのに15秒かかってしまう。  
    try {
        const val1 = await funcVal1();
        const val2 = await funcVal2();
        const val3 = await funcVal3();
    } catch(err) {
    }
  
    // この場合、await は使わず、Promise.all()を使って並列処理すれば  
    // main()が終わるのは5秒で済む。
    const func1 = funcVal1();
    const func2 = funcVal2();
    const func3 = funcVal3();
  
    Promise.all([func1, func2, func3]).then(([val1, val2, val3]) => {
        // すべて正常終了した際の処理
    }).catch((err) => {
        // どれか一つでもErrorがスローされた際の処理
    });
}

また急ぎ足気味になってしまいましたが、今日はこの辺で。

【Javascript】非同期処理のPromiseやasync/awaitについて その1

Promiseについて

Promiseとは、

  • 非同期処理を制御するためのしくみ
  • ES2015以降で使用可能

メリット

その1. コードが簡潔になり、見やすくなる。(個人差あり)

例えば、非同期関数func1〜func3があったとして、下記の制約がある場合、

  • func2はfunc1の結果が必要
  • func3はfunc2の結果が必要

一般的なコールバック関数を使用した場合、ソースは下記のようになる。
(典型的な「コールバック地獄」というやつです)
※しかもエラー処理を省略してるので、エラー処理も書いたらもっと煩雑になる...

function main() {  
    func1((err, data) => {
        func2(data, (err2, data2) => {
            func3(data2, (err3, data3) => {
                console.log(data3);
            });
        });
    });
}

これがPromiseを使えば、例えばこうなります。

// resolveの引数は、正常終了した際の戻り値。  
// rejectの引数は、エラー発生した際の戻り値。

function funcPromise1() {
    return new Promise(function(resolve, reject) {
        func1((err, data) => {
            if(!err) {
                resolve(data);
            } else {
                reject(err);
            }
        });
    });
}  

function funcPromise2(data) {
    return new Promise(function(resolve, reject) {
        func2(data, (err, data2) => {
            if(!err) {
                resolve(data2);
            } else {
                reject(err);
            }
        });
    });
}  

function funcPromise3(data2) {
    return new Promise(function(resolve, reject) {
        func3(data2, (err, data3) => {
            if(!err) {
                resolve(data3);
            } else {
                reject(err);
            }
        });
    });
}  

function main() {  
    funcPromise1.then((data) => {return funcPromise2(data);})  
        // resolveされた場合、then()の引数にはresolveの引数が格納される。  
        // rejectされた場合、catch()の引数にはrejectの引数が格納される。
        .then((data2) => { return funcPromise3(data2); })
        .then((data3) => { console.log(data3); })
        .catch((err) => { console.log(err); });
}

funcPromise1~funcPromise3の関数定義は増えましたが、ソース自体は見やすくなったと思います。

エラー処理が簡潔になる

(先程のソースで若干ネタバレしてますが)func1〜func3のいずれかでエラーが発生しても、Promiseなら最後の「catch(err)」でまとめてエラー処理ができます。

これがコールバック関数の場合、こうなります。
(クッソ見にくい...というか、try~catchを使った場合、全体を囲ってtry~catchではエラー捕捉ができない(=毎回外側にthrowしなくちゃいけない)ので、もっと大変なことに。)

function main() {  
    func1((err, data) => {
        if(!err) {
            func2(data, (err2, data2) => {
                if(!err2) {
                    func3(data2, (err3, data3) => {
                        if(!err3) {
                            console.log(data3);
                        } else {
                            console.log(err3);
                        }
                    });
                } else {
                    console.log(err2);
                }
            });
        } else {
            console.log(err2);
        }
    });
}

非同期処理の並行実行の制御ができる

Promiseを使った場合、例えば複数の非同期処理を同時実行した場合に、下記のような処理が可能です。

    
function main() {  
    // Promise.race()は、引数の非同期処理のうち、
    // いずれか1個の処理が完了したら次へ進む
    Promise.race([funcPromise, funcPromise2, funcPromise3])
         // 最初に終わった処理がresolveの場合、thenの引数dataに
         // その処理のresolve値が格納される。
         // また最初に終わった処理がrejectの場合、catchの引数errに
         // その処理のreject値が格納される。
        .then((data) => { console.log(data); })
        .catch((err) => { console.log(err); });
  
    // Promise.all()は、引数の非同期処理がすべて終了するまで次へ進まない。
    Promise.all([funcPromise, funcPromise2, funcPromise3])
        // 引数data~data3には、それぞれfuncPromise~funcPromise3の
        // resolve値が格納される。(すべてresolveしないとthen()は実行されない)
        .then(([data, data2, data3]) => { 
            console.log(data); 
            console.log(data2);
            console.log(data3);
        })
        // いずれか1つでもrejectされた場合、最初にrejectされた値の
        // reject値が格納される。
        .catch((err) => { console.log(err); });
}

そして、話はasync/awaitにつながる訳ですが、それはまた後日。