JavaScriptの名前空間とモジュール分割方法 require(node.js/browserify) HTMLScriptタグ WSH フル対応

  • 5
    いいね
  • 0
    コメント

タイトルが長くなりました。

名前空間、モジュール分割。

これだけでJavaScript業界(界隈)はひどく混乱しているような気がしました。

自分はクラスタイプの.NET系経験のある、JavaScriptを初めて1ヶ月程度の者です。

他言語では迷う事のない言語の根本的な名前空間とかスタティッククラスモジュール分割的な機能が、JavaScriptだと歴史的経緯として実装がさまざまになってしまっている事に驚きました。

どうにか、混乱しないようにしておかないといけないな、と思ったのでまとめておきます。

例えば、'名前1.名前1-1.名前1-1-1'、みたいな'文字列'をピリオドで分解してNameSpaceとして定義する、というような実装は複雑なので採用しませんでした。

ちなみに環境はWindowsですが、最後のWSH JScript 部分以外はどんな環境でも使えると思います。ほとんどのファイルはUTF-8 BOM あり CRLF形式で動かしています。
UTF-8 BOM 無し LF 形式でも問題なく動くでしょう。

ライブラリ部分

まず、ベースになるライブラリ部分です。
ネームスペースとしてns1.ns1_1 などを定義して
そこにprivateな変数や関数、publicな変数や関数を書いてみました。

libA.js
//nsはNameSpaceの省略

//名前空間ns1を定義
var ns1 = ns1 || {};
(function (global) {
  var _ = ns1;
  //上記までが名前空間の先頭定義

  //名前空間ns1_1を定義
  _.ns1_1 = ns1.ns1_1 || {};
  (function () {
    var _ = ns1.ns1_1;
    //名前空間の先頭定義終わり

    //private
    var privateValue01 = 1;
    var privateFunc01 = function () {
      return -2;
    };

    //public
    _.publicValueNum01 = 2;
    _.publicValueStr01 = 'A';
    _.publicFunc01 = function () {
      return -4;
    };

    //メソッド(public)を定義する
    _.plus = function (valueA, valueB) {
      return valueA + valueB + 
        (privateValue01*2) + privateFunc01() + 
        (_.publicValueNum01*2) + _.publicFunc01();
    };

  }()); //名前空間ns1_1の終わり

}(this)); //名前空間ns1の終わり


if (typeof module !== 'undefined') {
  module.exports = ns1;
}

ns1.ns1_1.plusとして、関数が定義されています。
渡された引数2つを加算するものです。
余計なものがついているのは、privateやpublicの値をメソッド内でちゃんと使えるかどうか確認しています。

名前空間の作成部分

var ns1 = ns1 || {};    

で、既存のネームスペースとぶつかっても安全なようになっています。この書き方単独なら、名前空間で検索すればみつかります。

関数での変数スコープの限定

名前空間の定義直後に次の記述がきます。

(function (global) {
:
}(this)); 

この書き方も、検索すればわかると思います。無名の即時実行関数で、グローバル変数を定義しています。
ns1ではglobalを定義していますが、ns1_1では引数のない無名の即時実行関数です。
変数スコープを確保するために作っています。

requireのない環境への対策

最後のここ、

if (typeof module !== 'undefined') {
  module.exports = ns1;
}

これは、node.js や browserify 用のrequireに対するコードの
module.exports の記述です。requireに対応していない環境だと'定義されてないよ!'とエラーになるので定義されている時だけ実行するようになります。

ライブラリ部分(拡張)

ライブラリを拡張する時は次のように書きます

libB.js
if (typeof require !== 'undefined') {
  var ns1 = require('./libA.js')
}

//名前空間ns1を定義
var ns1 = ns1 || {};
(function (global) {
  var _ = ns1;

  //名前空間ns1_1を定義
  _.ns1_1 = ns1.ns1_1 || {};
  (function () {
    var _ = ns1.ns1_1;

    //private
    var privateValue02 = 1;
    var privateFunc02 = function () {
      return -2;
    };

    //public
    _.publicValueNum02 = 2;
    _.publicValueStr02 = 'A';
    _.publicFunc02 = function () {
      return -4;
    };

    //メソッド(public)を定義する
    _.plusPlus = function (valueA, valueB) {
      return _.plus(valueA, valueB) + 
        _.plus(valueA, valueB) + 
        (privateValue02*2) + privateFunc02() +
        (_.publicValueNum02*2) + _.publicFunc02();
    };

  }());

}(this));

if (typeof module !== 'undefined') {
  module.exports = ns1;
}

先頭ではrequireでlibA.jsを呼び出しています。requireがない環境にも対応しています。

名前空間の、ns1.ns1_1は同じですが内部のPrivateとかPublicを新しく定義しています。

関数plusPlusも定義しています。内部では、libA.jsの pulsメソッドを呼び出しています。

ライブラリを使用するメイン部分

mainModule.js
if (typeof require !== 'undefined') {
  var ns1 = require('./libB.js')
}


//alertのない環境(node.js)に対応するために実装
if (typeof alert === 'undefined') {
  alert = function (message) {
    console.log(message);
  };
}

//渡された2つの値が不一致ならメッセージを出す関数を用意する
//requireを使用しない場合、グローバルを汚染するが動作確認のためだけに使う。
function check(a, b) {
  if (a !== b) {
    alert('a:' + a + '\n' +
      'b:' + b );
  }
}

var main = function () {
  //libAから
  check(3, (ns1.ns1_1.plus(1, 2)));
  check(2, ns1.ns1_1.publicValueNum01);
  check('A', ns1.ns1_1.publicValueStr01);
  check(-4, ns1.ns1_1.publicFunc01());

  check('object', typeof ns1);                          //名前空間
  check('object', typeof ns1.ns1_1);                    //名前空間
  check('undefined', typeof ns1.ns1_1.privateValue01);  //private
  check('undefined', typeof ns1.ns1_1.privateFunc01);   //private
  check('number', typeof ns1.ns1_1.publicValueNum01);   //public
  check('string', typeof ns1.ns1_1.publicValueStr01);   //public
  check('function', typeof ns1.ns1_1.publicFunc01);     //public
  check('function', typeof ns1.ns1_1.plus);             //public

  //libBから
  check(6, (ns1.ns1_1.plusPlus(1, 2)));

  alert('test finish テスト終了');
  return 'main finish';
};

if (typeof module !== 'undefined') {
  module.exports = main;
}

alertを定義したり、check関数で動作確認しています。

ライブラリ内部ではalertを使う事を想定しているので、alert定義の無い場合は
console.log で動いてください、という処理を入れています。
console.logだけでよい場合はalertを書き換えたりする必要はないでしょう。

動作確認

素のHTML

runHtml.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title></title>

    <script src="./libA.js"></script>
    <script src="./libB.js"></script>
    <script src="./mainModule.js"></script>
    <script>

  document.addEventListener("DOMContentLoaded",function(eve){
      document.body.innerHTML += main();
  },false);

    </script>
  </head>
  <body>
  </body>
</html>

普通に、scriptタグで記述しています。
scriptを書く順番は間違えないでください。たぶん、この並びじゃないと動かない気がします。
表示させると、test finish というメッセージが表示されます。

browserifyで変換後のスクリプトを指定したHTML

browserifyのコマンドは次のとおりです。

>browserify -r ./mainModule.js:main -o ./build/build.js

buildフォルダにbuild.jsを出力しています。
build.jsの中身は見る必要がないので掲載しません。

runHTML_browserify.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title></title>

    <script src="./build/build.js"></script>
    <script>

  var main = require('main');

  document.addEventListener("DOMContentLoaded",function(eve){
      document.body.innerHTML += main();
  },false);

  window.onload = function(){

      document.body.innerHTML += main();
  };

    </script>
  </head>
  <body>
  </body>
</html>

これで動きます。
test finish というメッセージが表示されます。

node.js で動かす

node.js用のモジュールを書きます。

runNodeJs.js
var main = require('./mainModule.js');

main();

これを、コマンドで呼び出すと test finish が出力されます

    node runNodeJs.js

WSH

今時 wsh もないと思うのですが、でも、捨て去るのももったいない気もするので、こちらも対応します。

WSH wsf形式

さて、WSHです。
alertがないので、ここで定義しています。
たぶんconsole.logもないので、WScript.Echo使ってます。

runWsh.wsf
<?xml version="1.0" encoding="UTF-8" ?>
<job>
    <script language="JavaScript" src="./libA.js"></script>
    <script language="JavaScript" src="./libB.js"></script>
    <script language="JavaScript" src="./mainModule.js"></script>

    <script language="JavaScript">
    <![CDATA[
//----------------------------------------

//wshではalertがないので定義する
function alert(messageText) {
    WScript.Echo(messageText);
}

main();

//----------------------------------------
    ]]>
    </script>
</job>

このファイルもUTF-8 BOMあり CRLFです。
拡張子、wsfだと、windows上で起動も可能だしrequire的なのが簡単です。
これでファイルをダブルクリックするだけで動作します。

WSH jse形式

WSHでは、こういう実行も可能です。
まあ、.jsでも動くのですが、わかりやすいように .jse という拡張子にするみたいです。

require的な動作をさせるために、evalを使うしかないようなのと、このファイルだけSJISじゃないとWindowsで動かなかったと思うので、SJISにしています。
SJISからUTF-8を読み込ませるために長々と書いています。

runWsh.jse
var fso = new ActiveXObject("Scripting.FileSystemObject")
var basePath = fso.GetParentFolderName(WScript.ScriptFullName);

//wshではalertがないので定義する
function alert(messageText) {
  WScript.Echo(messageText);
}

//テキスト ファイル 入出力
var encodingTypeJpCharCode = {
  NONE                :0,
  ASCII               :1,
  JIS                 :2,
  EUC_JP              :3,
  UTF_7               :4,
  Shift_JIS           :5,
  UTF8_BOM            :6,
  UTF8_BOM_NO         :7,
  UTF16_LE_BOM        :8,
  UTF16_LE_BOM_NO     :9,
  UTF16_BE_BOM        :10,
  UTF16_BE_BOM_NO     :11
}

function getEncodingTypeName(encodingType) {
  switch (encodingType) {
  case encodingTypeJpCharCode.Shift_JIS: 
    return "SHIFT_JIS";

  case encodingTypeJpCharCode.UTF16_LE_BOM:
    return "UNICODEFFFE";
  case encodingTypeJpCharCode.UTF16_LE_BOM_NO:
    return "UTF-16LE";

  case encodingTypeJpCharCode.UTF16_BE_BOM:
    return "UNICODEFEFF";
  case encodingTypeJpCharCode.UTF16_BE_BOM_NO:
    return "UTF-16BE";

  case encodingTypeJpCharCode.UTF8_BOM:
    return "UTF-8";
  case encodingTypeJpCharCode.UTF8_BOM_NO:
    return "UTF-8N";

  case encodingTypeJpCharCode.JIS:
    return "ISO-2022-JP";

  case encodingTypeJpCharCode.EUC_JP:
    return "EUC-JP";

  case encodingTypeJpCharCode.UTF_7:
    return "UTF-7";
  }
}

function string_LoadFromFile(filePath, encodingType) {
  var result = '';
  var encordingName = getEncodingTypeName(encodingType)

  var stream = new ActiveXObject('ADODB.Stream');
  stream.Type = 2;        //(adTypeText = 2)
  switch (encodingType) {
  case encodingTypeJpCharCode.UTF8_BOM_NO: 
    stream.Charset = getEncodingTypeName(EncodingTypeJpCharCode.UTF8_BOM);
    break;
  default: 
    stream.Charset = encordingName;
    break;
  }
  stream.Open();
  stream.LoadFromFile(filePath);
  result = stream.ReadText();
  stream.Close();
  return result;
}

var includeFileName = "./libA.js"
eval( 
  string_LoadFromFile(
    fso.BuildPath(basePath, includeFileName), 
    encodingTypeJpCharCode.UTF8_BOM));

var includeFileName = "./libB.js"
eval( 
  string_LoadFromFile(
    fso.BuildPath(basePath, includeFileName), 
    encodingTypeJpCharCode.UTF8_BOM));

var includeFileName = "./mainModule.js"
eval( 
  string_LoadFromFile(
    fso.BuildPath(basePath, includeFileName), 
    encodingTypeJpCharCode.UTF8_BOM));
//--------------------------------------------------
//SHIFT_JISの場合下記のようにする
//eval( 
//    fso.OpenTextFile(
//        fso.BuildPath(basePath, includeFileName), 1)
//    .ReadAll() );
//--------------------------------------------------

main();

これで動きます。

こんな感じで、様々な環境で動く JavaScript モジュールを作る事ができました。
libA.js や libB.js や mainModule.js を真似て自作のライブラリを作れば、ほぼすべての環境で動かすことができるのではないでしょうか。

参考にした記事

旧石器時代のJavaScriptを書いてる各位に告ぐ、現代的なJavaScript超入門 Section1 ~すぐにでも現代っぽく出来るワンポイントまとめ~ - Qiita
JavaScriptの設計について考える - 機能ごとに分類する / Lei Hau'oli Engineer's Blog | レイハウオリエンジニアブログ
既存 JS ファイルを Browserify 用に書き直す - Qiita
自作ライブラリをrequreされてもそうでなくても使えるようにする - Qiita
【脱初心者JavaScript】名前空間のイロハ - Qiita
ブラウザ、Node.jsの両方で使えるJavaScriptの書き方 - Qiita

最後に

そのような環境で動かせるライブラリを目指して、GitHubで公開しています。
ご参考にしてください。

stsLib.js/Source/stsLib.js