RSS2.0

npm でパッケージ管理された JavaScript 開発をしてみる

ある程度の規模の開発をしていくと、避けては通れないのが共通ロジックのパッケージ化と、どのパッケージを利用するかという依存性の管理です。Java には maven や gradle, Python には pip や setuptools があるように、JavaScript ではしばらく前から npm が利用されています。npm は Node.js が提供するパッケージ管理ツールで、Node.js 自体を利用しない JavaScript 開発でも十分に利用価値があります。自分で書いたコードをパッケージ化することでロジックの共有や使い回しが簡単になりますし、自分のロジックが利用している他パッケージのバージョン管理をすることで動作の保証やパッケージのバージョンアップを厳密に行えるようになります。
dotall.png
今回はこの npm コマンドを使って、既に提供されているライブラリを依存関係を管理しつつ利用していこうと思います。

ちなみに npm ではパッケージとモジュールという用語が定義されています。
内容をかいつまむと、npm パッケージは package.json と呼ばれるファイルとプログラムを配置したディレクトリのこと、npm モジュールは require 構文によって他のソースコードから読み込まれるライブラリのことです。自分が開発するものが npm パッケージ、他のエンジニアが開発したものが npm モジュールという感じでしょうか。主体が変わることで呼び方が変わるわけですね。

npm をインストールする

いつもどおり yum コマンドで npm をインストールしましょう。
# yum install npm
これで終わりです。

npm パッケージを作る

npm パッケージのソースコードを置いておくディレクトリを作成します。
ここでは dotall という名前のパッケージを作りたいので、ディレクトリ名も dotall としておきます。
$ mkdir dotall
$ cd dotall

作成したディレクトリ内で npm init コマンドを実行すると、npm パッケージのメタ情報を表す package.json ファイルが自動生成されます。
途中、対話形式で package.json に記載される内容について確認されますが、初期値で問題なければ未入力のまま Enter キーを押していけば良いです。
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (dotall) dotall
version: (1.0.0) 1.0.0
description: dot pictures from text files.
entry point: (index.js) dotall.js
test command: 
git repository: 
keywords: 
author: 
license: (ISC) MIT
About to write to /home/momokan/javascript/dotall/package.json:

{
  "name": "dotall",
  "version": "1.0.0",
  "description": "dot pictures from text files.",
  "main": "dotall.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT"
}


Is this ok? (yes) yes
これにより package.json ができあがります。ファイルの中身は npm init 時にコンソールで出力されたものそのままになっているはずです。

ライセンスの入力箇所が気になったので調べてみたのですが、入力できる値はSPDX license expressionと呼ばれるもののみが入力できるようです。ライセンスを正確に表すための一意な名称(というかコード)です。
ちなみに初期値となっている ISC は Internet Software Consortium のライセンスらしいですが、よくわからないので MIT License にしておきます。ここはパッケージ作成者が決めればよいものです。

エントリーポイントとなるソースコードの初期値は index.js になっていました。npm パッケージではこれが通例のようですが、ここではパッケージ名を入れておきたいので dotall.js とします。
あとは、この dotall.js を作成すれば、npm パッケージとして必要なファイルが揃ったことになります。

他の npm モジュールを利用する

JavaScript には他の JavaScript ファイルを読み込んでその中の関数を利用する、というような仕組みはありません。しかし npm はそれを可能にしていて、自分の書いたソースコードから第三者の書いた 3rd party ライブラリを利用することができます。
さきほど作った自分の npm パッケージで、jQuery を npm モジュールとして利用してみます。

以下のように npm install コマンドで、ローカル環境に jquery をインストールすることができます。
$ npm install jquery --save
実行すると node_modules ディレクトリ内にインストールした npm モジュールが配置されます。

また --save オプションを指定すると、同時に package.json に依存性が追記されます。
$ cat package.json
{
  "name": "dotall",
  ...
  "dependencies": {
    "jquery": "^3.3.1"
  }
}

インストールした npm モジュールを JavaScript のソースコード上で読み込むには、require 構文を使います。
jquery npm モジュールの場合、以下のように返り値の変数を jquery の $ にみたてて利用することができます。
var $ = require('jquery');

$(function(){
    alert('Page is loaded.');
});

require 構文が利用できるようになったので、今回作る npm パッケージのソースコード dotall.js を用意します。
var $ = require('jquery');

Dotall = function() {
    this.canvas = null;
    this.schema = null;
    this.actionState = null;
    this.dotSize = 4;
    this.defaultColor = "rgba(0, 0, 0, 0)";
    this.canvasCss = {
        'position': 'absolute',
        'top':      window.innerHeight / 2 + 'px',
        'left':     window.innerWidth / 2 + 'px',
        'z-index':  1024
    };
};

Dotall.prototype.load = function(schema) {
    this.schema = schema;
    this.prepareCanvas();
    this.action();
};

Dotall.prototype.loadUrl = function(url) {
    var dotall = this;

    $.getJSON(url, function(data) {
        dotall.load(data);
    });
};

Dotall.prototype.prepareCanvas = function() {
    if (this.canvas != null) {
        return;
    }

    var canvasId = 'dotall-canvas-' + Math.floor(Math.random() * (1000000));

    $('body').append('<canvas class="dotall" id="' + canvasId + '"/>');

    this.canvas = $('#' + canvasId);
    this.canvas.css(this.canvasCss);
};

Dotall.prototype.paint = function(imageNumber = 0) {
    var context = this.canvas[0].getContext('2d');
    var pixels = this.getPixelsOf(imageNumber);
    var y = 0;

    // canvas 描画前にリサイズしておく必要がある
    this.resizeCanvas(imageNumber);

    for (var lineNumber in pixels) {
        var line = pixels[lineNumber];
        var x = 0;

        for (var column in line) {
            var colorNumber = line[column];

            if (colorNumber != ' ') {
                context.fillStyle = this.getColorOf(colorNumber);
                context.fillRect(x * this.dotSize, y * this.dotSize, this.dotSize, this.dotSize);
            }

            x = x + 1;
        }

        y = y + 1;
    }
};

Dotall.prototype.resizeCanvas = function(imageNumber = 0) {
    var pixels = this.getPixelsOf(imageNumber);
    var lineLength = 0;

    for (var lineNumber in pixels) {
        if (lineLength < pixels[lineNumber].length) {
            lineLength = pixels[lineNumber].length;
        }
    }

    this.canvas.attr('width', lineLength * this.dotSize);
    this.canvas.attr('height', pixels.length * this.dotSize);
}

Dotall.prototype.getImages = function() {
    if (this.schema == null) {
        console.warn('image data is not loaded.');
        return [];
    }
    
    if (this.schema.images == null) {
        console.warn('image data dose not have "iamges" property.');
        return [];
    }

    return this.schema.images;
}

Dotall.prototype.getPixelsOf = function(imageNumber) {
    var image = this.getImages()[imageNumber];

    if (image == null) {
        console.warn('image data dose not have iamge of index: ' + imageNumber);
        return [];
    }

    if (image.pixels == null) {
        console.warn('image data dose not have "pixels" property.');
        return [];
    }

    return image.pixels;
}

Dotall.prototype.getColorOf = function(colorNumber) {
    if (this.schema == null) {
        console.warn('image data is not loaded.');
        return this.defaultColor;
    }
    
    if (this.schema.colors == null) {
        console.warn('image data dose not have "colors" property.');
        return this.defaultColor;
    }

    if (this.schema.colors[colorNumber] == null) {
        console.warn('image data dose not have color of index: ' + colorNumber);
        return this.defaultColor;
    }

    return this.schema.colors[colorNumber];
}

Dotall.prototype.action = function() {
    var pixelPerSecond = 100;
    var imageRefreshInterval = 1000;
    var currentTime = new Date().getTime();

    if (this.actionState == null) {
        this.actionState = {
            "imageNumber": 0,
            "imageRefreshTime": currentTime,
            "motionStartTime": null,
            "motionStartPoint": {
                "top": parseInt(this.canvas.css('top').replace(/px$/, '')),
                "left": parseInt(this.canvas.css('left').replace(/px$/, ''))
            },
            "motionEndTime": currentTime,
            "motionType": "top",
            "motionValue": 0
        }
    }

    // 表示を更新する
    if (this.actionState.imageRefreshTime <= currentTime) {
        this.paint(this.actionState.imageNumber);
        this.actionState.imageRefreshTime = this.actionState.imageRefreshTime + imageRefreshInterval; 
        this.actionState.imageNumber = (this.actionState.imageNumber + 1) % this.getImages().length;
    }

    // 移動する
    if (this.actionState.motionEndTime <= currentTime) {
        // 現在地を終点に補正する
        this.canvas.css(this.actionState.motionType, this.actionState.motionStartPoint[this.actionState.motionType] + this.actionState.motionValue + "px");

        // 開始点を更新する
        this.actionState.motionStartTime = this.actionState.motionEndTime;
        this.actionState.motionStartPoint = {
            "top": parseInt(this.canvas.css('top').replace(/px$/, '')),
            "left": parseInt(this.canvas.css('left').replace(/px$/, ''))
        };

        // 次の移動時間を決める
        var second = Math.floor(Math.random() * (3));

        // 次の移動量を求める
        this.actionState.motionEndTime =  this.actionState.motionEndTime + (second * 1000);
        if (Math.floor(Math.random() * (100)) % 2 == 0) {
            this.actionState.motionType = "top";
        } else {
            this.actionState.motionType = "left";
        }
        this.actionState.motionValue = second * pixelPerSecond;
        if (Math.floor(Math.random() * (100)) % 2 == 0) {
            this.actionState.motionValue = this.actionState.motionValue * -1;
        }
    } else {
        // 現在地を更新する
        var motionStartValue = this.actionState.motionStartPoint[this.actionState.motionType];
        var motionTime = this.actionState.motionEndTime - this.actionState.motionStartTime;
        var motionCurrentTime = currentTime - this.actionState.motionStartTime;

        this.canvas.css(this.actionState.motionType, motionStartValue + (this.actionState.motionValue * (motionCurrentTime / motionTime)) + "px");
    }

    // 次の更新処理を呼び出す
    var dotall = this;
    setInterval(function() {dotall.action();}, 10);
}
dotall.js 自体のソースコードについてはここでは補足しません。とりあえず自作の JavaScript であるという程度で話を先に進めていきます。

browserify で npm パッケージをビルドする

インストールした他の npm モジュールを読み込むために利用されている require 構文ですが、これは JavaScript でサポートされている構文ではありません。つまり、このままではこのソースコードは JavaScript として実行することができません。
これをブラウザが実行できるソースコードに変換するのが npm モジュールの browserify です。

まずは npm install コマンドで browserify をインストールします。
$ npm install browserify --save-dev
ネット上ではローカルマシン内ならどこからでも利用できるよう、-g オプションをつけてグローバルにインストールする例が多いようですが、ここでは作っている npm パッケージ内でのみ参照できるようにしています。browserify 自体にもバージョンがあるので、実際に開発時に利用している browserify を他の npm パッケージと共有せずに固定したほうが安全だろうというのが理由です。

jquery をインストールした時に指定した --save オプションの代わりに、--save-dev オプションを指定しています。これは開発用の依存性を package.json に追加するという意味です。通常の依存性として追加されている npm モジュールはビルドすると成果物に含まれますが、開発用の依存性として追加された npm モジュールはビルド成果物に含まれません。browserify は npm パッケージをビルドするための npm モジュールなので、ビルドした成果物に含める必要はありません。
$ cat package.json 
{
  "name": "dotall",
  ...
  "dependencies": {
    "jquery": "^3.3.1"
  },
  "devDependencies": {
    "browserify": "^16.1.1"
  }
}
package.json を見てみると、dependencies ではなく devDependencies に browserify が追記されているのがわかります。

実際に browserify で npm パッケージをビルドするには、以下のコマンドを実行します。
$ $(npm bin)/browserify dotall.js -o dotall-$(node -p "require('./package.json').version").js
長いですね。

ところどころに出てくる $( ) という表現は、Linux のシェルの文法で、( ) 内部のコマンドを実行した結果を置換するという動きになります。
npm bin コマンドは、インストールされている npm モジュールの実行ファイルのパスを返します。npm モジュールを --save オプションを指定してインストールすると、npm モジュールに含まれる実行ファイルは node_modules/.bin/ 配下に配置される仕様になっていて、インストールされた browserify の配置先を得るために $(npm bin)/browserify という書き方になっています。

browserify の -o オプションで指定されているのは、ビルドした npm モジュールの出力先ファイルです。
ここでも Linux シェルの $( ) という表現が登場しています。( ) 内部の node コマンドは Node.js を実行するもので、-p オプションで指定した文字列をそのままソースコードとして実行します。ここでは package.json を読み込んで書かれている version 値を得られるようにしてあります。package.json にはライブラリのバージョンとして 1.0.0 が記述されているので、browserify によってビルドされたファイルは dotall-1.0.0.js になります。

browserify された npm モジュールを使う

browserify でビルドされた npm モジュールは、HTML の script タグで読み込んで使うことができます。
以下のような HTML を作成してブラウザで表示してみます。
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
</head>
<body style="background: #E0E0E0;">

<script src="dotall-1.0.0.js"></script>
<script type="text/javascript">
var dotall = new Dotall();

dotall.load(
    {
        "colors": [
            "rgb(255, 255, 255)",
            "rgb(190, 190, 190)",
            "rgb(170, 170, 170)"
        ],
        "images": [
            {
                "pixels": [
                    " 2222222 ",
                    "200000002",
                    "210101012",
                    "200000002",
                    " 2121212 ",
                    "  2 2 2  "
                ]
            },
            {
                "pixels": [
                    "  22222  ",
                    " 2000002 ",
                    "201000102",
                    " 2000002 ",
                    "  20202  ",
                    " 212 212 "
                ]
            }
        ]
    }
);
</script>

</body>
</html>
dotall.js は Dotall.load() メソッドに指定したデータからドット絵を構築して表示する JavaScript です。
dotall.js の細かい仕様については今回は詳しい説明は省きますが、無事動作していれば、画面内を這い回る何かが表示されたと思います。

  JavaScript  コメント (0)  2018/04/10 19:20:00


公開範囲:
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2018, 10>>
30123456
78910111213
14151617181920
21222324252627
28293031123