20190927のNode.jsに関する記事は5件です。

LamdaでS3に上がっている画像を一括でリサイズする(ローカルから実行可能)

目的

S3に上がっている画像ファイルをリサイズしたい
 → Webサイトの画像DLの負荷を下げる、画面表示の高速化をする

Lamdaの実装

Lamdaの基本的な設定は端折ります。
S3にアップされたファイルを別のバケットにリサイズして作成するAPIを以下の通り実装。

!!同じバケットを指定すると無限実行されるので注意!!

index.js

index.js
var AWS = require('aws-sdk');
var gm = require('gm').subClass({ imageMagick: true });
var util = require('util');
var s3 = new AWS.S3();

exports.handler = function(event, context, callback) {

  var bucket = event.Records[0].s3.bucket.name; // 変換元画像のバケット名
  var key = event.Records[0].s3.object.key;     // 変換元画像のキー名
  var thumbnailBucket = bucket + "-thumbnail";  // 変換後画像のバケット名(ここでは末尾にthumbnailを付け足すことにしてある)
  var thumbnailKey = key;                       // 変換後画像のキー名(ここでは変換前と同じにしてある)

  // 拡張子を抽出
  var typeMatch = key.match(/\.([^.]*)$/);
  if (!typeMatch) {
    callback("Could not determine the image type.");
    return;
  }

  // 拡張子がjpg, jpeg, pngのものだけ許可する
  var imageType = typeMatch[1];
  if (imageType != "jpg" && imageType != "jpeg" && imageType != "png") {
    callback('Unsupported image type: ${imageType}');
    return;
  }

  // S3の画像にアクセス
  s3.getObject({Bucket: bucket, Key: key}, function(err, data){
    if (err) { context.done('error getting object', err); }

    // ----- Icon用 -------
    gm(data.Body)
    .resize('40', null) // 40pxにする
    .stream(function(err,stdout,stderr){ // ストリームで出力する
      if(err){
        context.fail(err);
      }
      var chunks = []; // ストリームのチャンクを溜め込むための配列
      stdout.on('data',function(chunk){
        chunks.push(chunk); // チャンクを溜め込む
      });
      stdout.on('end',function(){
        var buffer = Buffer.concat(chunks); // 最後まで溜め込んだら結合してバッファに書き出す
        gm(buffer).toBuffer(function(errs, buff){
          var params = {
            Bucket: thumbnailBucket,
            Key: 'icon/'+thumbnailKey,
            ContentType: imageType,
            Body: buff
          };

          s3.putObject(params, function(err, data) { // S3に書き出す
            if(err){
              console.log("gm upload error");
              context.fail(err);
            }
            context.succeed({
              "error":false
            });
          });
        });
      });

      stderr.on('data',function(data){
        console.log('stderr data: ' +  data);
      });
    }); // Icon Create End

  });
};

gnu-sedのインストール

payload.jsonのファイル名を書き換えられるように以下インストール

$ brew install gnu-sed

参考:gnu-sedのインストール

invokeコマンドを連続実行するシェルスクリプトを書く

resize.sh
filename=$1
cat ${filename} | while read line
do
  gsed -i -e "30c\\          \"key\": \"${line}\"," payload.json
  aws lambda invoke --invocation-type Event --function-name {*lamdaName*} --region ap-northeast-1 --payload file:///Users/{userName}/develop/payload.json output.txt
  echo ${line}
done

payload.jsonを作成する

payload.json
{
  "Records":[  
    {  
      "eventVersion":"2.0",
      "eventSource":"aws:s3",
      "awsRegion":"ap-northeast-1",
      "eventTime":"1970-01-01T00:00:00.000Z",
      "eventName":"ObjectCreated:Put",
      "userIdentity":{  
        "principalId":"AIDAJDPLRKLG7UEXAMPLE"
      },
      "requestParameters":{  
        "sourceIPAddress":"127.0.0.1"
      },
      "responseElements":{  
        "x-amz-request-id":"C3D13FE58DE4C810",
        "x-amz-id-2":"FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
      },
      "s3":{  
        "s3SchemaVersion":"1.0",
        "configurationId":"testConfigRule",
        "bucket":{  
          "name":"{バケット名}",
          "ownerIdentity":{  
            "principalId":"A3NL1KOZZKExample"
          },
          "arn":"arn:aws:s3:::{バケット名}"
        },
        "object":{  
          "key": "{対象ファイル名 *シェルで書き換えらる}",
          "size":1024,
          "eTag":"d41d8cd98f00b204e9800998ecf8427e",
          "versionId":"096fKKXTRTtl3on89fVO.nfljtsv6qko"
        }
      }
    }
  ]
}

リサイズしたいファイルをtxtにまとめる

images.txt
user/file1.png
user/file2.png
user/file3.png

実行コマンド

$ . resize.sh images.txt

実行すると202のステータスが返ってきますが、キューに送られただけで正常なので大丈夫です。

まずはLamda単体をテスト実行してから動かしていきましょう。

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jsのビルドで「Module not found: Error: Can't resolve 'regenerator-runtime/runtime'」が出た時の対策

「Module not found: Error: Can't resolve 'regenerator-runtime/runtime'」が出た

async/awaitを使っているコードをビルド(babelをかけたら)したら
以下のようなエラーとなる場合がある。

Module not found: Error: Can't resolve 'regenerator-runtime/runtime' in xxx

対策案その1 core-jsとregenerator-runtimeを導入する

Babel 7.4.0以降を使っているなら、

2つのnpmモジュールcore-jsregenerator-runtimeをインストール

npm install core-js@3 regenerator-runtime

async/awaitを使っているソースコード(.js)でいまインストールしたモジュールをimportする

import "core-js/stable";
import "regenerator-runtime/runtime";
・・・以下略・・・

対策案その2 @babel/polyfill を導入する

@babel/polyfillをインストール

npm install --save-dev @babel/polyfill

async/awaitを使っているソースコード(.js)で import "@babel/polyfill" する

import "@babel/polyfill";
・・・以下略・・・

babel公式によると、@babel/polyfillBabel 7.4.0で deprecatedとなっているので注意

対策案その3 対象ブラウザを変更する

変更前>古いブラウザを対象にした@babel/preset-envの設定

※targetsに注目

webpack.config.js抜粋
{
 test: /\.js$/,
 exclude: /(node_modules|bower_components)/,
 use: [
  {
   loader: 'babel-loader',
   options: {
    presets: [
     [
      '@babel/preset-env',
      {
       'modules': 'false',//commonjs,amd,umd,systemjs,auto
       'useBuiltIns': 'usage',
       'targets': '> 0.25%, not dead',
       'corejs': 3
      }
     ]
    ]
   }
  }
 ],
}, 

変更後>新しいブラウザを対象にした@babel/preset-envの設定

※targetsに注目

webpack.config.js抜粋
{
 test: /\.js$/,
 exclude: /(node_modules|bower_components)/,
 use: [
  {
   loader: 'babel-loader',
   options: {
    presets: [
     [
      '@babel/preset-env',
      {
       'modules': 'false',//commonjs,amd,umd,systemjs,auto
       'useBuiltIns': 'usage',
       "targets": {
        "browsers": [
         "last 1 Chrome version",
         "last 1 Firefox version",
         "last 1 Edge version",
         "last 1 Safari version",
         "last 1 Opera version"
        ]
       },
       'corejs': 3
      }
     ]
    ]
   }
  }
 ],
},

(対策その2のほうは、babelの目的から考えると、本末転倒かな。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Libraのハッカソンにlibra-authというのを出してみた

Facebookの仮想通貨Libraが来年発行されるという事で、世界中の中央銀行はじめトランプさんなどからも袋叩きの今日この頃ですが、

まぁ、そういう政治的?というか大人の方々のお話はともかくとして、Libra使って何ができるのかいじってみたいというのはQiita界隈に生息するものの本能だったりもします。

で、先月そのLibraのデベロッパーグループの主催で行われたハッカソンHackLibraにLibra Authというプロジェクトで参加して、入賞は逃したけど「Honorable mention (名誉ある言及)」という賞的なものを頂いたので、その辺を少し書いてみようかなと思います。

最近Qiitaに全然書いてなかったし。

HackLibra
https://medium.com/@hacklibra/hacklibra-winners-8da1a9124628

自分のブログ「かも日記」 LibraのハッカソンHackLibraで3位入賞はできなかったけどなんか褒められた感?
https://jsgt.org/wp/?p=975

Libra Auth
https://github.com/toshirot/libra-auth

残念ながら締め切り日の4日後にLibra testnet の更新があって、私のデモが依存していたライブラリが動かなくなってしまいました。

まぁ、発展途上のLibraなのでしょうがないのですが、その修正(修正しても良いかはちゃんと聞いたのだけど)が締め切り後ということで減点になってしまったので3位入賞を逃してしまいました。あれが無ければ入賞していたと自負はしてます。えへん。

さて、Libra Authという名前でわかる通り、これはLibraを利用したauthenticationについての話です。

Libraの公開鍵と秘密鍵を利用した認証方法で、EdDSAによる署名を使用します。

Libraクライアントの鍵に基付く認証ですが、BobとAlice間の認証時の通信はLibraブロックチェーンを呼び出さなくても可能(呼び出せばより強固ですが)なので、Libraブロックチェーンに負荷を与えません。というわけで処理速度が速いです。

以下の図はこのDemoのものですが、ユーザー「ALICE」はLibraでチケットセンター「BOB」にチケット料金を支払い、署名チケットで認証されます。
alt

詳細は Github の説明を観ていただくとして、ざっくりいうと、Libraを送受信した「Alice」と「Bob」本人であることをお互いに証明できるPrivate Key とトランザクション上で参照できるPublic Key を利用して相互に本人確認し、Signatureで作ったQRコードで認証をする方法についてのテスト実装です。

このデモは、例えば入場券の販売などを想定しています。

手順

(1,2,3) AliceはBobのチケットセンターへ10Libra送金し、Bobへアドレスとメッセージを送ります。
(4,5) Bobは受け取ったメッセージと自分のPrivate Key でSignatureBを作りAliceへ送ります。
(6,7) Aliceは送ったメッセージと受け取ったSignatureBをBobのPublic Key でVerifyしてTrueならボブが、2のトランザクションでLibraを支払った相手であることが証明されます。
そこで、SignatureBとAliceのPrivate Key でSignatureAを作り、それとAliceのAddressでQRコードを作ります。
(8) Bobは入場ゲートでAliceのQRコードをスキャンし、送ったSignatureBと受け取ったSignatureAをAliceのPublic Key でVerifyしてTrueならAliceが、2のトランザクションでLibraをBobへ支払った人であることが証明されるので
(9) 証明された入場をOKにします。

上記の(1~4)以降はLibraネットワークを参照しなくても認証は可能なので、例えば入場門で数万人を連続処理してもLibraのネットワークに負荷をかけないということです。

もちろん、QRコードの盗難や転売対策として、(5~8)を繰り返すことも可能です。あるいは、転売を許可する代わりに、転売手数料がBobにも入る仕組みにすることもできると思います。

デモ

https://libra-auth.com/

ALICE: Buy ticket:
http://libra-auth.com/buy-ticket.html
BOB: Authenticate ticket:
https://libra-auth.com/auth-ticket.html

コードはNode.jsで書いてますが、基本的にはどんな言語でもかけるかなと思います。その辺はこれから少しいじるつもりなので、後日続きを書きたいなと思ってます。現状のDemoコードはGithubでご覧ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Azure Bot Service】QnA Makerのfollow-up promptに対応する

前提条件

Node.js の SDK v4 でBot Serviceを作っている。

QnA Maker のfollow-up promptに対応する

SDKの対応状況

QnA Maker には follow-up prompt という機能があるがpreview版なので、現在 Bot Service からダウンロードできるソースコードでは、まだ対応していない。
しかし、botbuilder-aiのソースコードを見ると、ライブラリの方ではこっそり対応されている。

https://github.com/microsoft/botbuilder-tools/issues/1272#issuecomment-525475729

上記リンクのコメントにもあるが、まだpreview版ということで、公式にはまだ対応したということになっていない。

StackOverflowに簡単なサンプルが載っている。
ただしC#で書かれていて、Node.jsで作っている場合は参考程度にしかならない。

https://stackoverflow.com/questions/57573086/what-piece-of-microsoft-botbuilder-samples-qnamaker-prompting-do-i-need-to-add-i/57577194#57577194

実装サンプル

まず、新たにjsファイルを作成し、QnA Makerから返ってきた回答にfollow-up promptが付いているか確認し、付いている場合は返答にボタンを追加する処理を作る。

qnaResultHelper.js
const { MessageFactory } = require('botbuilder');

/**
 * QnA Maker から返ってきた答えを適切な形にしてクライアントへ返す
 * 
 * @param {string} qnaResult QnA Maker から返ってきたanswer
 * @returns Activity
 */
module.exports.createAnswer = function(context, qnaResult) {

    if (qnaResult.context && qnaResult.context.prompts && qnaResult.context.prompts.length > 0) {
        // answerにfollow-up promptが付いている
        return createPrompt(qnaResult);
    } else {
        return qnaResult.answer;
    }
}

/**
 * follow-up prompt をクライアントへ返す
 * 
 * @param {QnAMakerResult} qnaResult 
 * @returns Activity
 */
function createPrompt(qnaResult) {
    let title = qnaResult.answer;
    let prompts = qnaResult.context.prompts;
    let buttons = [];

    // DisplayOrderで並べ替え
    prompts.sort(function(a, b) {
        if (a.displayOrder == b.displayOrder) {
            return 0;
        } else if (a.displayOrder < b.displayOrder) {
            return -1;
        }
        return 1;
    });

    // follow-up promptのボタンを作る
    for (let prompt of prompts) {
        buttons.push(prompt.displayText);
    }

    return MessageFactory.suggestedActions(buttons, title);
}

次に、既存のqnaBot.jsのonMessageの処理を修正し、回答を返す部分で、先ほど作ったHelperクラスのメソッドを呼び出すようにする。

qnaBot.js
this.onMessage(async (context, next) => {
    this.logger.log('Calling QnA Maker');

    const qnaResults = await this.qnaMaker.getAnswers(context);

    // If an answer was received from QnA Maker, send the answer back to the user.
    if (qnaResults[0]) {
        // 自作したHelperのメソッドを呼び出す
        let answer = QnAResultHelper.createAnswer(context, qnaResults[0]);
        await context.sendActivity(answer);

    // If no answers were returned from QnA Maker, reply with help.
    } else {
        await context.sendActivity('お答えできません。他の言い方で質問してみてください。');
    }

    // By calling next() you ensure that the next BotHandler is run.
    await next();
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webpackで複数プロジェクトの一括コンパイルと部分コンパイル

インデックス
* ディレクトリ毎にアプリケーションをコンパイル
* プロジェクト共通のライブラリを設定
* コンパイルするターゲットを決めてビルドする

要件
以下のようなディレクトリ構成があり、それぞれのプロジェクト毎にJavaScriptをコンパイルして、それぞれのプロジェクトディレクトリにコンパイルしたファイルを格納したいといった案件がありました。Webpackでプロジェクトをコンパイルする時、今までは一つのディレクトリにのみ全てのプロジェクトを書き出していたので、ディレクトリ単位に書き出す方法を調べました。また、すべてのプロジェクトではなく、特定のプロジェクトだけビルドする方法も合わせて調べた結果を記載します。

ディレクトリ構造
.
├── package.json
├── src
│   ├── common                <- プロジェクト共通で使うファイル
│   └── projects
│       ├── foo         <- 単一プロジェクト
│       ├── fuga              <- 単一プロジェクト
│       ├── hoge              <- 単一プロジェクト
│       └── pages             <- スペシャルプロジェクト
│           ├── pageOne    <- スペシャルプロジェクトに関連するプロジェクト 
│           ├── pageThree     <- スペシャルプロジェクトに関連するプロジェクト 
│           └── pageTwo       <- スペシャルプロジェクトに関連するプロジェクト 
└── webpack.config.js

通常であれば、webpack.config.jsoutputに以下のような設定をすると複数ページのアプリケーションとしてコンパイルしてくれます(参考)。

特定のディレクトリに別アプリケーションとして書き出し
module.exports = {
  //...
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  },  
  output: {
    filename: '[name].js',
    path: __dirname + '/dist'
  }
};

ディレクトリ毎にアプリケーションをコンパイル

今回は一つのディレクトリにまとめて書き出すのではなく、プロジェクトディレクトリ毎にそれぞれ書き出したい訳です。

それぞれのプロジェクトディレクトリに書き出し
│   └── projects
│       ├── foo
│       │   ├── app.js
│       │   ├── dist          <- ここに書き出して管理したい
│       ├── fuga
│       │   ├── app.js
│       │   └── dist          <- ここに書き出して管理したい
│       ├── hoge
│       │   ├── app.js
│       │   └── dist          <- ここに書き出して管理したい

そのため、少しトリッキーかもしれませんが、entryoutputの設定を変更し、
複数ディレクトリに書き出しできるような設定に変更しました。今回は各プロジェクトに distというフォルダを作成し、その中にコンパイル後のファイルを書き出します。イメージとしてはこんな感じです。

イメージ
module.exports = {
  //...
  entry : {
  './src/projects/path-to-project/dist/hashfilename.js': './src/projects/path-to-project/app.js',
  },
  output: {
    path: __dirname,
    filename: '[name]'
  }
};

output.pathは書き出す先のパスを指定します。今回は書き出す先のディレクトリをルート直下に指定し、ファイル名にパスを含めた書き出し先を指定しました。outputで使っている[name]はWebpack側で定義されている設定でentrykeyとなっている部分を置換します。なので上記の設定だと ./src/projects/path-to-project/dist/hashfilename.js[name]に入ることになります。

余談ですが、プロジェクト単位で作成されるdistディレクトリをgitの管理から外す場合は.gitignoreに以下のように記述すると良いです。

.gitignoreの設定
/**/dist/

プロジェクト共通のライブラリを設定

それぞれのプロジェクトで共通して使いたいクラスは以下の設定で使用可能になります。

ライブラリパスの設定
module.exports = {
  //...
  resolve: {
    module: [
      path.resolve(__dirname, './src/common'), 
    ];
  }
};

こうすると./src/common が名前空間になり、例えば./src/common/Common.jsのファイルをプロジェクトで使いたい場合、import文は以下の書式になります。

import Common from 'Common';

また、./src/common/particle/flower.jsの場合はそのままサブディレクトリを繋げれば良いです。

import Common from 'particle/flower';

コンパイルするターゲットを決めてビルドする

複数プロジェクトのビルドに対応しておけば、1つのプロジェクトだけビルドしたい場合や、2つのプロジェクトだけビルドしたい場合など、特定のプロジェクトに絞ってビルドすることも可能になります。
方法はnpm run buildとする際に、webpack.config.jsに環境変数(env)としてビルドターゲットのプロジェクト名を渡します。こんな感じです。

npm run build -- --env.target="hoge"

このコマンドには2つの意味が含まれているのですが、--env.target="hoge"の部分がwebpack.config.jsに環境変数(env)を渡している部分となります。
また、webpack.config.jsで環境変数(env)を受け取る場合、エクスポートを関数に変更する必要があります。
https://webpack.js.org/guides/environment-variables/

To use the env variable, you must convert module.exports to a function:

環境変数(env)の渡し方
webpack --env.NODE_ENV=local --env.production --progress
環境変数(env)の受け取り方
const path = require('path');

module.exports = env => {
  // Use env.<YOUR VARIABLE> here:
  console.log('NODE_ENV: ', env.NODE_ENV); // 'local'
  console.log('Production: ', env.production); // true

  return {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };
};

私の場合はenvtargetプロパティを追加し、targetにプロジェクトディレクトリがあれば、そのプロジェクトのみビルドするようにしました。なお、プロジェクトのファイルは全て統一してapp.jsしています。

// NOTE: Use function in order to pass environment variables.
// @see https://webpack.js.org/guides/environment-variables/
module.exports = (env, {watch} ) => {
  let entry = {};

  if (env && env.target) {
    // NOTE: Divide target string into array by comma.
    const targetFiles = env.target.split(',').map(s => s.trim())

    for (file of targetFiles) {
      // NOTE: ここでプロジェクト毎にentryを作成しています(割愛)
      const filePath = './' + join(targetDir, 'app.js');
      const anEntry = createEntry(filePath);
      Object.assign(entry, anEntry);
    }
  } else {
    // NOTE: 全てのエントリー情報
   entry = createAllEntry();
  }

  return {
    //...
    entry: entry,
    output: {
      path: __dirname,
      filename: '[name]'
    },    
  }
})  

ここでもうひとつnpm run build -- --env.target="hoge" 実行時のbuildの後の-- についてですが、これのダブルハイフン以降に定義する変数を実行時のファイルに変数として渡すという意味です。例えば、以下のnpmパッケージがあり、

package.json
 {
    script: 
    {
        server: "node index.js"
    }
 }

以下のコマンドを実行すると、index.jsに環境変数「--port=8080」が渡されます。

npm run server -- --port=8080

これは、通常のnodeコマンドを実行するときと同じ意味になります。

node server.js --port=1337

npmコマンドに渡す変数と、スクリプトに渡す変数の2つの違いがあるということですね。

Conclusion

今回は複数プロジェクトをビルドする時にプロジェクト毎にディレクトリを指定してビルドする方法と、プロジェクトの一部だけビルドする方法を調査しました。部分ビルドについてはプロジェクトが膨大になった時にビルド時間を短縮するために有効そうです。この部分ビルドはwatchオプションをつけた時にも適用されるので、開発していくときに便利かもしれません。

今回の検証で使ったプロジェクトはGihubに置いてありますので、何かの参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む