まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2018/08

マンホールマップでも使っている、 jqPuzzle を使って任意画像をスライドパズル化する方法を紹介します。

このスライドパズルは「15パズル」とも呼ばれていて、僕くらいのオッサンは↓こんなのがおもちゃ売り場で売られているのをよく目にしました。「懐かしいゲーム」の1つです:
2018083000



この jqPuzzle を使ったスライドパズル機能はマンホールマップ内の全てのマンホール画像で遊べます。例えばこのマンホール画像ページの「スライドゲームに移動」をクリックすると:
2018083001


紹介されているマンホール画像がこんなスライドパズルに早変わり:
2018083002


"shuffle" ボタンをクリックするとランダムにシャッフルされます。16 が空いた状態でパズルスタートです:
2018083003


空いたピースの上下左右にあるピースをクリックすると、そのピースが空いた部分にスライドして移動します。これを繰り返して 1 から 15 までが正しい位置にくる完成を目指す、というものです。個人的な印象としては1、2、3までは簡単だけど、4を揃える所あたりからコツが必要になってくると思ってます:
2018083004


こんな楽しい機能を提供する jqPuzzle は jQuery を併用して、画像にスライドパズルのインターフェースを追加してくれる CSS および JavaScript のセットです。なお、jqPuzzle が対応する jQuery は 1.x までの模様なので、この点のみ注意が必要です:
2018083001


jqPuzzle を使うには公式サイトから zip ファイルをダウンロード&展開して使います(CDN は見当たりませんでした)。なお jqPuzzle の提供ライセンスは 以下の通り、GPL と MIT のデュアルライセンス、だそうです:
2018083002


利用にあたっては jQuery 1.x をロードした後に CSS と JavaScript をロードします。これで準備完了(以下の例では jQuery v1.6.2 を指定しています。また jqPuzzle の両ファイルはこれを記述する HTML と同じ階層に存在しているものとします):
<script type="text/javascript" src="http://code.jquery.com/jquery-1.6.2.min.js"></script>
<link rel="stylesheet" type="text/css" href="jquery.jqpuzzle.css"/>
<script type="text/javascript" src="jquery.jqpuzzle.packed.js"></script>

一番簡単な利用方法は <img> タグに jqPuzzle クラスを指定する方法だと思います。実はこれだけでその画像は 4x4 のスライドパズル化されて表示されます:
<img src="sample.jpg" class="jqPuzzle"/>

2018083005


ちなみに "Original" ボタンをクリックすると完成形が、"Numbers" をクリックすると各ピースの数字の表示/非表示が切り替わります。数字表記がないと難易度は一気に上がります。


カスタマイズの要素を加えることも可能です。例えば以下の例では 4x4 で 16 番目の駒を抜くことは変えずに、ボタンの文字を日本語化し、最初からシャッフル済みになるようにしています(というわけでシャッフルボタンも不要なので非表示にしました):
  :
<script type="text/javascript">
var settings = {
  rows: 4,
  cols: 4,
  hole: 16,
  shuffle: true,
  numbers: true,
  language: 'ja',
  control: {
    shufflePieces: false,
    confirmShuffle: true,
    toggleOriginal: true,
    toggleNumbers: true,
    counter: true,
    timer: true,
    pauseTimer: true
  },
  success: {
    fadeOriginal: false,
    callback: undefined,
    callbackTimeout: 300
  },
  animation: {
    shuffleRounds: 3,
    shuffleSpeed: 800,
    slidingSpeed: 200,
    fadeOriginalSpeed: 600
  },
  style: {
    gridSize: 2,
    overlap: true,
    backgroundOpacity: 0.1
  }
};
var texts = {
  shuffleLabel: 'シャッフル',
  toggleOriginalLabel: '元画像',
  toggleNumbersLabel: '数値表示/非表示',
  confirmShuffleMessage: 'シャッフルしてよろしいですか?',
  movesLabel: '回',
  secondsLabel: '秒'
};

$(function(){
  var t = $('img.jqPuzzle');
  t.jqPuzzle( settings, texts );
});
</script>
 :
2018083006


ちょっとした息抜き機能を追加するのに便利なライブラリです。


Node-RED を使うことで IoT データの収集や Web API の実装などが非常に簡単に実現できます。このブログでも何度か紹介していますし、公開されている外部モジュールを使って更にカスタム機能を追加することも可能です。

今回紹介するのは HTTP in ノードに認証機能を追加する node-red-contrib-httpauth ノードです。これを使うと Node-RED に標準装備されている HTTP in ノード(HTTP リクエストノード)に Basic 認証や Digest 認証を簡単に追加することができるようになります:
2018082900


実際に使う場合は、Node-RED の画面右上のメニューから「パレットの管理(Manage Pallette)」を選びます:
2018082901


設定ダイアログが表示されたら、"Palette" の "Install" タブで "httpauth" と検索します。すると node-red-contrib-httpauth ノードが見つかるので、"install" ボタンをクリックしてノードを追加します:
2018082902


インストールが成功すると以下のような表示になります。ここから実際にノードが使えるようになります:
2018082903


この時点でパレットにも "http auth" というノードが追加されていることが確認できます:
2018082904


実際に使う場合、http in ノードの直後に http auth ノードを配置します。この例では http in ノードの直後に http auth ノードを配置し、その後ろに(いつも使っているような)template ノードや function ノードを配置して、最後に http response ノードで HTTP リクエスト可能な API を作りました:
2018082905


template ノードの中身はシンプルにしています(認証が成功するとこの文字列が表示される、というテストです):
2018082906


そして http auth ノードに認証内容を設定します。この例では Basic 認証でレルム文字列は MyRealm 、そしてユーザー名 : user1 &パスワード : pass1 を設定しました。この状態でデプロイします:
2018082907


デプロイ後にウェブブラウザでこの API にアクセスすると、先程設定した http auth が機能し、指定した内容の認証が行われます。具体的にはユーザー名とパスワードを問い合わせるダイアログが表示され、先程指定した内容が入力されないと先へ進めません:
2018082908


上記で設定した内容(ユーザー名 : user1、パスワード : pass1)が正しく入力されると HTTP リクエストが正しく実行され、設定していた文字列が表示されます:
2018082909


この http auth ノードを使うことで、Node-RED で作成する API や Web ページに簡単に Basic 認証をかけることができそうです。


 

IBM Cloud から提供されているサーバーレス環境である Cloud Functions は、オープンソースの Apache OpenWhisk をベースとした FaaS(Functions as a Service)となっており、数あるサーバーレス環境の中でもよりオープンなものとなっています:
2018082301


IBM Cloud の Cloud Functions の場合、専用のダッシュボードが用意されており、手元に開発環境を用意することなく、ここから(ブラウザから)簡易的なアクションを記述/保存/実行することもできるようになっています:
2018082302
 ↑Node.js でアクションを記述している様子


Cloud Functions はこのブラウザからアクションを記述して実行することができる、というメリットがあるのですが、個人的にはここで記述できるアクションはかなり限定的なものだと思っています。理由はここで編集できるものはアクションとして実行する関数の中身だけであって、関連する設定ファイルを変更したりすることはできません。Node.js では npm という強力な外部モジュール連携の仕組みがあり、npm を使って標準の Node.js では提供されていない機能を呼び出して利用することができます。が、この仕組を使うには npm コマンドであらかじめ利用する機能を導入しておくか、 package.json と呼ばれる設定ファイルを変更して利用を宣言しておく必要があります。実行する関数の記述を変更するだけでは対応しきれない仕組みがあり、この機能に関してはこのウェブブラウザからのアクション記述だけでは使えないのでした。


前置きが長くなりましたが、ここからが本エントリの本番です。この外部パッケージを使ったアクションの記述は CLI(Command Line Interface)を使うことで実現できます。その手順を紹介します。

まず利用したい外部パッケージを明示した package.json を用意します。この例では "request" という HTTP クライアントパッケージ(のバージョン 2.88.0)を利用することと、エントリーポイントとなる JavaScript ファイル名(main.js)を指定しています:
{
  "name": "requestAction",
  "version": "0.0.1",
  "main": "main.js",
  "dependencies": {
    "request": "^2.88.0"
  }
}

次にエントリーポイントとなる JavaScript ファイル main.js を以下の内容で用意します。この中で request パッケージを require して使っている点に注目してください:
// main.js
async function myAction( params ){
  try{
    const result = await getHtml( params );
    return result;
  }catch( err ){
    return err;
  }
}

function getHtml( params ){
  return new Promise( function( resolve, reject ){
    if( params.url ){
      const request = require( 'request' );

      var options = {
        method: 'GET',
        url: params.url,
        encoding: null
      };
      request( options, function( err, res, buf ){
        if( err ){
          reject( { status: false, error: err } );
        }else{
          var html = buf.toString( 'utf-8' );
          resolve( { status: true, html: html } );
        }
      });
    }else{
      reject( { status: false, error: 'parameter url is needed.' } );
    }
  });
}

exports.main = myAction;

このアクションでは実行時に url パラメータを指定します(未指定の場合はエラー)。指定された url の HTML コンテンツを request パッケージを使って取得し、その結果を return します。 なお request() は非同期に実行される関数のため、HTML コンテンツを取得する部分だけを関数化(getHTML())し、この関数は Promise オブジェクトを(実行結果が得られたら)返すようにしています。 なお、このアクションは Node.js V8 で実行することを想定しているので、getHTML() 関数を実行する際に async/await を使って非同期に呼ぶようにしています。

この2つのファイル(package.json と main.js)でアクションに必要な設定と内容が用意できました。なお、同じファイルをこちらに用意しておいたので、興味ある方は参照ください:
https://github.com/dotnsf/requestAction


ではこのアクションを実際に IBM Cloud Functions にデプロイして実行するまでの手順を紹介します。今回用意したスクリプトはブラウザからデプロイするのではなく、 CLI (ibmcloud コマンド)を使ってデプロイする必要があります。というわけで、まだ CLI の導入ができていない場合で最初に CLI をデプロイする必要があります。

【ibmcloud コマンド CLI のインストール&セットアップ】
Windows や MacOS、Linux 向けの ibmcloud コマンドは以下のページからインストールできます:
https://console.bluemix.net/docs/cli/reference/ibmcloud/download_cli.html

Windows はダウンロードしてインストール、MacOS や Linux であれば以下のコマンドを実行してインストールします:
$ curl -sL http://ibm.biz/idt-installer | bash

ibmcloud コマンドが導入できたら IBM Cloud Functions を使うためのプラグインをあわせて導入/更新しておきます:
$ ibmcloud plugin install cloud-functions -r Bluemix

また、このあとのコマンドを実行する際に IBM Cloud にログインしている必要があるため、この段階で ibmcloud コマンドによる IBM Cloud へのログインとターゲットの設定を済ませておきます:
$ ibmcloud login

$ ibmcloud target --cf


【Node.js および npm のインストール】
ブラウザからアクションを記述する場合はクライアントに Node.js をインストールする必要はなかったのですが、今回の CLI を使った手順ではローカルで npm コマンドを実行することになります。つまり Node.js や npm がインストールされた環境が必要です。まだ導入していない場合は、公式ページなどから(V8 以上の Node.js の)ダウンロード&インストールを済ませておいてください:
http://nodejs.org/


【ライブラリのインストール、zip ファイル化、デプロイ】
まず package.json の内容にしたがってパッケージ(今回のケースでは request パッケージ)とその依存ライブラリをインストールします:
$ npm install

次にこのディレクトリ内の全ファイルを zip ファイルにまとめます。IBM Cloud Functions では zip ファイルでまとめたアクションをデプロイすることができるため、ここで実行に必要な全てのファイルが揃った zip ファイル(myAction.zip)を作成します:
$ zip -r myAction.zip *

そして、この zip ファイルをアクションとして ibmcloud コマンド CLI でデプロイします:
$ ibmcloud wsk action create requestAction myAction.zip --kind nodejs:8

↑この例ではアクションの名前を "requestAction" として myAction.zip をデプロイしています(アクション名やファイル名は任意です)。なおこの方法でアクションをデプロイする場合、--kind オプションで実行ランタイムの種類を指定する必要があります。今回は Nodejs V8 を指定しています(V6 では未対応の機能を使っているためです)。 また新規にデプロイする場合はこのコマンドになりますが、アクションを変更して同名で上書きデプロイする場合は上記の create 部分を update に変更して実行してください。


【動作確認】
デプロイしたアクションは CLI でもダッシュボード画面からも、どちらでも動作を確認することができます。まずはダッシュボードで確認してみます。ダッシュボードから Actions メニューを選び、アクション一覧の中にデプロイしたアクション(requestAction)が含まれていることを確認します:
2018082401


デプロイしたアクションを選択します。zip ファイルという特殊(?)な方法でデプロイしたので、この画面から直接コードを参照したり変更することはできないのですが、実行(Invoke)したり、実行時の入力パラメータを編集することはできます。今回のアクションは url パラメータを受け取って動作するので、実行前にパラメータを指定します。"Change Input" をクリックします:
2018082402


入力パラメータを編集する画面が表示されます。今回は何らかの url パラメータを指定したいので、
{ "url": "http://dotnsf.blog.jp/" }
のように何らかの実在する(パスワード等なしで参照できる)URL を url パラメータに指定して "Apply" をクリックします:
2018082403
 ↑このブログの URL を指定した例


改めて "Invoke" をクリックして、このアクションを先程の入力パラメータで実行します:
2018082404


アクションが実行され、少し待つと結果の JSON が表示されます。正しく実行されていれば status = true と、指定した URL から取得した HTML が返ってくることが確認できます:
2018082405


全く同じことを ibmcloud CLI からも実行してみます。CLI の場合は以下のようにパラメータを指定して実行し、その実行結果を確認します(実行そのものは --result オプションなしでも行えますが、--result オプションをつけると実行結果が同じ画面に出力され、確認ができます)。期待していたような HTML が表示されれば確認成功です:
$ ibmcloud wsk action invoke requestAction --param url "http://dotnsf.blog.jp/" --result

2018082406


とりあえず、本来の目的であった「Cloud Functions で npm 外部パッケージを使ったアクションを実行する」方法については実現できそうだ、という目処がたちました。


これまであまり積極的に使うことのなかった webpack を使ってみました:
2018082300


webpack は複数のモジュール(ファイル)を1つに(場合によっては複数に)まとめるバンドラ(bundler)ツールです。Node.js でいうと、エントリーポイントとなる main.js ファイルと、ここから呼ばれるサブ機能が sub1.js や sub2.js などの別ファイルに分かれている場合に、これら3つのファイルを1つのファイルにまとめるというものです。

【サンプルの紹介】
今回は jsonwebtoken パッケージを使ったシンプルなサンプルコード(main.js)を用意しました:
// main.js
const jwt = require( 'jsonwebtoken' );

function main( params ){
  const id = params.id;
  const password = params.password;
  const token = jwt.sign( { id: id, password: password }, 'secret' );

  console.log( token );
}

main({
  id: 'userid',
  password: 'password'
});

global.main = main;

↑ main() 関数の中で2つのパラメータ(id と password)を受け取り、JSON をトークン化して出力する、というものです。そして id = 'userid', password = 'password' を指定して main を実行しています。

この main.js を実行する前に、コード内で使っている jsonwebtoken パッケージをインストールする必要があります。以下の内容の package.json を用意します:
{
  "name": "jwt_test",
  "version": "0.0.1",
  "main": "main.js",
  "dependencies": {
    "jsonwebtoken": "^8.3.0"
  }
}

そして以下のコマンドで jsonwebtoken パッケージをインストールします:
$ npm install


その後に main.js を普通に実行すると、{ id: 'userid', password: 'password' } をトークン化した結果が表示されます:
$ node main.js
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRvdG5zZiIsInBhc3N3b3JkIjoicGFzc3dvcmQiLCJpYXQiOjE1MzQ5OTU5NTh9.LUx0q5ycwIFeR1HNnNFgUrlX0w0cny0qDVsPmiVtVnI

とりあえずこの状態で動くことは確認できました。コードは main.js 1つだけに記述されていますが、jsonwebtoken パッケージを利用しているので、実行前に $npm install (jsonwebtoken) を実施しておく必要があり、実際にはここでインストールされるモジュールファイルも使われながら動作します。これらを1ファイルにまとめた上で実行できるようにするというのが今回の目的です。


【webpack のインストール】
webpack は npm を使ってインストールします。例えば以下のコマンドでグローバルインストールすることができます:
$ npm install webpack -g

今回は webpack を(このプロジェクト用に)ローカルインストールして使うことにします。なお webpack 4.0 以降では webpack-cli も合わせて導入する必要があるのでまとめてインストールし、webpack コマンドが実行できるようローカルモジュールへのパスを通します:
$ npm install webpack webpack-cli

$ export PATH=$PATH:./node_modules/.bin


【config ファイルの用意】
webpack を使ってバンドルする作業の条件を config ファイルと呼ばれるファイルで指定します。以下の内容で webpack.js ファイルを作成します:
var path = require( 'path' );
module.exports = {
  entry: './main.js',
  output: {
    path: path.resolve( __dirname, 'dist' ),
    filename: 'bundle.js'
  },
  target: 'node'
};

↑ main.js をエントリーポイント(最初に実行するファイル)とする一連の処理の中で必要とされるモジュール(今回の例では jsonwebtoken パッケージ)がバンドルの対象となり、バンドル結果は dist/bundle.js というファイルに出力される、という指定をしています。


【バンドルと実行】
ここまでに準備した環境とファイルを使って webpack を使ってみます。

まずはバンドル、上記で作成した webpack.js を config ファイルとして指定して webpack コマンドを実行します:
$ webpack --config webpack.js

バンドルが成功すると dist/bundle.js というファイルが生成され、この1ファイルに main.js と jsonwebtoken パッケージがバンドルされているはずです。

確認のため生成されたファイルを実行します。念の為、これらのパッケージモジュールが存在しないテンポラリディレクトリ(/tmp)に bundle.js ファイル1つだけをコピーした上で実行します:
$ cp dist/bundle.js /tmp
$ cd /tmp
$ node bundle.js
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRvdG5zZiIsInBhc3N3b3JkIjoicGFzc3dvcmQiLCJpYXQiOjE1MzUwMDM1MjF9.llZuuWxqX607AWrJBvMEDpFksFX8IvigUrdIWbhItRg

期待通りにトークンが表示される結果になりました。この bundle.js 1ファイルの中に jsonwebtoken パッケージやその依存パッケージまでが含まれる形でバンドルされて実行されたことが確認できました。


webpack を使うことで今回の例のように依存関係ごとファイルをまとめることができるだけでなく、ファイルのモジュール化が促進されたり、一度のロードで全ファイルを読み込めることから SPA(Single Page Application) が作りやすくなったりします。その一方で、バンドル先のファイルが大きくなってしまうと最初のロードに時間がかかるようになる、という問題もあります。個人的にはパッケージ化されたあとに開発コンソールを使ったデバッグが難しくなることに懸念も持っています。

まあケース・バイ・ケース、なんだろうなあ。。

 

Node.js を使ったアプリケーション開発中に npm install を実行して、こんなエラーに遭遇することがあります(個人的な印象では下の例のように canvas モジュールをインストールしようとした際によく遭遇します):
$ npm install canvas

> canvas@1.6.11 install /Users/dotnsf/src/tmp/node_modules/canvas
> node-gyp rebuild

gyp WARN download NVM_NODEJS_ORG_MIRROR is deprecated and will be removed in node-gyp v4, please use NODEJS_ORG_MIRROR
gyp ERR! configure error 
gyp ERR! stack Error: Python executable "/Users/dotnsf/.pyenv/shims/python" is v3.6.5, which is not supported by gyp.
gyp ERR! stack You can pass the --python switch to point to Python >= v2.5.0 & < 3.0.0.
gyp ERR! stack     at PythonFinder.failPythonVersion (/Users/dotnsf/.nvm/versions/node/v8.9.4/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:492:19)
gyp ERR! stack     at PythonFinder. (/Users/dotnsf/.nvm/versions/node/v8.9.4/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:474:14)
gyp ERR! stack     at ChildProcess.exithandler (child_process.js:267:7)
gyp ERR! stack     at emitTwo (events.js:126:13)
gyp ERR! stack     at ChildProcess.emit (events.js:214:7)
      :
      :

スクリーンショット 2018-08-21 9.44.07


Node.js でネイティブモジュールのビルドが必要になった場合に node-gyp を使ってビルドが行われるのですが、その際に使われる Python のバージョンがあっていない、というエラーです。ちと厄介なのは Python のバージョンが古くて問題になっているのではなく、新しすぎてエラーが発生している、ということです(上の例では v2.5.0 以上 v3.0.0 未満でないといけないのに v3.6.5 がインストールされていてエラーが発生している、というメッセージが表示されています)。

この解決のためにわざわざ古いバージョンをインストールしないといけないのか、ということはなく、以下のようにバージョンを明示して npm install することで解決できます:
$ npm install canvas --python=python2.7

> canvas@1.6.11 install /Users/dotnsf/src/tmp/node_modules/canvas
> node-gyp rebuild

gyp WARN download NVM_NODEJS_ORG_MIRROR is deprecated and will be removed in node-gyp v4, please use NODEJS_ORG_MIRROR
      :
      :

+ canvas@1.6.11
added 2 packages in 10.027s


このページのトップヘ