[javascript]spreadSheet5 ネームスペース

自前コードなので、関数やクラスをネームスペースにバラ巻いていた。

ただ、このままでは、再利用する度に、困りそうなので、カスタムタグのソースのクラスなどをMYAPPに纏めてみた。

まず、ネームスペースをまとめる関数を追加。※ネットで見つけたコードを少し改良したもの

/**
 *  MYAPP   グローバルオブジェクトのアプリ名
 */
const MYAPP = {};   //  初期設定

/**
 *  指定ネームスペースを作成する
 * @param {string} pNameSpace       作成するネームスペース・パス
 * @param {undefined | JSON} pJson  追記する内容 undefined または JSON
 * @param {boolean} pFreeze         変更不可指定
 */
MYAPP.namespace = (pNameSpace, pJson = undefined, pFreeze = false) => {
    /**
     * 再帰的にオブジェクトを凍結する
     * @param {object} obj  凍結するオブジェクト
     */
    const freezeR = (obj) => {
        if (typeof obj === 'object') {
            // オブジェクトの配下を再帰的に凍結
            for (const key of Object.keys(obj)) {
                freezeR(obj[key]);
            }
            // オブジェクト自体を凍結
            Object.freeze(obj);
        }
    };
    // ネームスペースのテキストを .で区切った配列を作成する。※先頭がMYAPPなら削除する
    const aNameSpace = pNameSpace.split('.').filter((element, index) => index !== 0 || element !== 'MYAPP');
    //  現在のパスをMYAPPへ移動する
    let curPath = MYAPP;
    //  パス最後の要素の有無で処理が分岐する為、配列から最後の要素を分離する。※constでもpopはオブジェクトを差替(代入)えないのでOK(らしい
    const lastElement = aNameSpace.pop();
    //  配列の順にネームスペースの有無をチェックする
    for (const part of aNameSpace) {
        // パスが存在しなければ作成し、パス位置を現在位置へ移動する
        curPath = curPath[part] = curPath[part] ?? {};
    }
    // パス最後の要素が未設定なら空リストを代入
    if (curPath[lastElement]) {
        // pJsonをマージする
        Object.assign(curPath[lastElement], pJson ?? {});
    } else {
        // pJsonに差替える
        curPath[lastElement] = pJson ?? {};
    }
    //  編集不可指定ありの場合
    if (pFreeze) {
        //  作成したネームスペースを再帰的に凍結します
        if (!pJson) {
            freezeR(curPath[lastElement]);
        } else {
            //  JSON配下のオブジェクトを再帰的に凍結します
            for (const key of Object.keys(pJson)) {
                freezeR(curPath[lastElement][key]);
            }
        }
    }
};

aNameSpaceからそのままfor (const part of aNameSpace)を回すと、curPathが最後まで移動し、pJsonをネームスペースに追加しようとすると、

 curPath = pJson ?? {} ;                            // MYAPPの方は書き換わらない

となってしまい、MYAPPの方は書き換わらないので、最終の一歩手前でcurPathを止めて、

curPath[lastElement] = pJson ?? {};                 // curPath[lastElement]はMYAPPの一部なのでOK

としたかったので、aNameSpaceから最後の要素を切り取った。

また、Object.assignの第一パラメータの中身がundefinedの場合は、エラってしまうので、

try {
  let obj = undefined;
  Object.assign(obj, pJson ?? {}); 
} catch(ex) {
  debug.print('Fail. '+ex);
}

⇒ Fail. Uncaught TypeError TypeError: Cannot convert undefined or null to object

undefined判定で空リストに変えてマージ。

    if (!curPath[lastElement]) {
        curPath[lastElement] = {};                      // 空リストにする
    }
    Object.assign(curPath[lastElement], pJson ?? {});   // マージする

さすがに

Object.assign(curPath[lastElement]??{}, pJson);   // マージする

は無理。

また、参照先が同じJSONにある場合は未確定な状態になりやすいので、MYAPP.namespaceの呼び出しを参照元と参照先で分けるのも面倒なので、第一パラメータで非参照先のネームパスを作成した方が楽。

これで完了と思ったら( ^ω^)・・・

MYAPP.namespace('aaa.bbb.ccc',
   (p1, p2, p3) => {
     ...
   }
);

aaa.bbb.ccc(1,2,3);

Uncaught TypeError TypeError: aaa.bbb.ccc is not a function

Object.assignは関数をプロパティっぽく書き込み、参照時に未定義な関数のエラーになるので、

    // パス最後の要素が未設定なら空リストを代入
    if (curPath[lastElement]) {
        // pJsonをマージする
        Object.assign(curPath[lastElement], pJson ?? {});
    } else {
        // pJsonに差替える
        curPath[lastElement] = pJson ?? {};
    }

で落ち着いた。

※まずpJsonがJSONかどうか判定した後の処理にした方が意図が読み取りやすいけど、深く意味を読み取ると無駄だと判るコードになる。

元ネタでは、

/**
 * スプレッドシートクラス
 * @class MYAPP.spreadSheet.SpreadSheetCustomElement
 */
MYAPP.namespace('spreadSheet.SpreadSheetCustomElement'),
MYAPP.spreadSheet.SpreadSheetCustomElement = class extends HTMLElement {
    ...
};

と、ネームスペースを作成し、作成したネームスペースにクラス等を設定する感じになっていた。

これを実際に動かしてみると、クラスの#変数がエラるw(パラメータがクラス宣言部なせいだろう)し、ネームスペースの文字も2度書いてるw(被ってるのは嫌い)なので、ネームスペースはファイルのパスまでとし、クラス名等はJSON形式でキー指定した方に変更して、クラスの定義は、こんな感じになった。

/**
 * スプレッドシートクラス
 * @class MYAPP.spreadSheet.SpreadSheetCustomElement
 */
MYAPP.namespace('spreadSheet', {
  'SpreadSheetCustomElement': class extends HTMLElement {
    ...
  }
});

ゴチャ付いてないから見やすくなった。(と思う

クラスを使用する場合はグローバルにクラスを宣言してないので、フルパス名で指定する。

_spreadSheet._command = new MYAPP.spreadSheet.SpreadSheetCommand(this);

関数も同様で、無名関数「function(…) { } 」でもいい。

MYAPP.namespace('lib', {
  'createHtmlElement': (elementName, options) => {
    ...
    },
});

使う時はフルパス名で指定する。

_layout.divTableN = MYAPP.lib.createHtmlElement('div', ... );

オブジェクトのプロパティはObject.freeze(object)で変更不可にできる。

メソッド名プロパティの追加プロパティの削除プロパティの値の変更
Object.preventExtensions()
Object.seal()
Object.freeze()

Object.freeze(object)以外は値の変更ができるので今回は対象外。

MYAPP.namespace('lib', {
  'char': {
    BR:'<br/>',
    CR: '\n',
    TAB: '\t',
    },
},true);             // 変更不可を指示

関数やクラスの外では意図的にstrictモードにしないとエラー(TypeError)が起きない。

ちなみに

"use strict";    // strictモード全開!

//  const 変数
MYAPP.namespace('lib', {
    'char': {
        BR: '<br/>',
        CR: '\n',
        TAB: '\t',
    },
}, true);
MYAPP.lib.char.BR = 'aaaaa'; ☚ここでType Error

Uncaught TypeError TypeError: Cannot assign to read only property 'BR' of object '#<Object>'

修正した結果

デバッグして止めて、スクリプト・スコープの内容を見ると、修正が漏れ過ぎ( ´∀` )

やっとのことでMYAPPにまとまった。

ネームスペースはすっきりした。

しかし、こんなコードを見た試しが無い。(大笑

パス名が付くのでコードが長くなる上、VSCodeの自動でコメントを生成するプラグインが怪しい動きをする可能性があるが、このクラス、変数はどこ?と探すのは楽かもしれない。

ps.2024/10/8

アップロード処理でvalueの無いセルデータでエラっていたので修正




コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA