0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ServiceNow】ClientScriptからUIScriptを呼んでみた【UIScript】

Last updated at Posted at 2025-12-20

この記事は ServiceNow アドベントカレンダー 2025 の12月6日分の記事 (シリーズ2) として執筆しています。

はじめに

何も考えずにClientScriptを書いているとstateのマジックナンバー(1,2,3...)とかリテラルがちらほら出てくるようになってしまったのでUIScriptを使って定数値を変数で持ちたいなぁと思って色々試したことを備忘録として残します。
今回はWorkspaceとcore UIで動作する事を確認していますが、ServicePortalとかでは検証できてないのでご了承ください。
なぜなら私はServicePortalを触った事がないからです。

PDI環境

試したPDI環境はこんな感じです。
Version: Zurich
image.png

Studioからアプリケーションを作成

PDIでお試し用のアプリを作成します。
image.png

スコープアプリで開発される事が多いと思うのでスコープにしておきます。
image.png

ロールもデフォルトのままにしておきます。
image.png

先にテーブルとWorkspaceを作っておきます。
image.png

適当にTaskを継承しておきます。
image.png

Workspaceも作っておきます。
これは基本的にcontinueを押すだけです。
image.png

これで検証用の準備が整いました。

UIScriptを作成して呼び出してみる

最初に呼び出し用のUIScriptを作成します。
Workspaceでも動かすのでUI TypeはAllにしておきます。

一旦、初期状態で保存します。
いつも思うのですが、初期状態で保存してパターンと合ってないって警告表示されるのは何故なのでしょうか?
(´・ω・`)イッタイドウイウコトナノ
image.png

定数値を共通化する為のUIScriptとして作成したのがこちらになります。
関数を返すのが想定だとは思いますがこういう形でも呼び出しが可能のようです。
上部の自己実行関数でWorkspace(WEP)用に作成し、下部の直下に同様のコードを記述する事でCoreUIにも対応できるみたいです。

AppConstants (UIScript)
// Replace "yourFunctionName" with an appropriate function name
(function () {
	var AppConstants = AppConstants || {};

	AppConstants = Object.freeze({
		REQUIRED: '必須項目です。',
		INVALID_EMAIL: 'メールアドレスの形式が正しくありません。',
		INFO_SAVED: '保存しました。'
	});

  return {
	// Define your Workspace functions here
	AppConstants : AppConstants
  };
})();

// Define your core UI functions here
var AppConstants = AppConstants || {};

AppConstants = Object.freeze({
	REQUIRED: '必須項目です。',
	INVALID_EMAIL: 'メールアドレスの形式が正しくありません。',
	INFO_SAVED: '保存しました。'
});

次に呼び出し側のClientScriptを作成します。

  • Name: OnLoadCallUiScriptTest
  • Table: <検証用に作成したテーブル>
  • UI Type: All
  • Type: onLoad
OnLoadCallUiScriptTest (ClientScript)
function onLoad() {
   //Type appropriate comment here, and begin script below
   var scriptName = 'x_1603370_uiscri_0.AppConstants';

    if (typeof g_ui_scripts !== 'undefined') {
        // Workspace(WEP)
        g_ui_scripts.getUIScript(scriptName).then(function(result) {
            console.log(result);
            g_form.addInfoMessage(result.AppConstants.INVALID_EMAIL);
        });
    } else {
        // クラシックUI
        ScriptLoader.getScripts(scriptName + '.jsdbx', function() {
            console.log(AppConstants);
            g_form.addInfoMessage(AppConstants.INVALID_EMAIL); // グローバルを直接参照
        });
    }
}

ClientScript内でg_ui_scriptsが定義されているかどうかでWorkspaceかCore UIかを判断しています。こうしておかないと両方で同時に動作することができないようです。。。

Workspaceでフォームを開いてみてaddInfoMessageが動作しているか確認します。
image.png
UIScriptを読み込んで動作できていそうです。

一応、Core UI側でも試しておきます。
こっちも大丈夫そうです。
image.png

こういう形で色々なClientScriptからUIScriptを呼べるのであればマジックナンバーやハードコーディングを減らせるかもしれません。

定数値を関数で返すUIScriptも作成してみる

デフォルトは関数を返すのがUIScriptの想定に思えるので関数で定数値を返すUIScriptも作って試してみます。
特別な事はなくて、単純に関数の戻り値でオブジェクトを返すだけです。

AppConstants2 (UIScript)
// Replace "yourFunctionName" with an appropriate function name
(function () {
	return {
		// Define your Workspace functions here
		getConst: function() {
			return Object.freeze({
				REQUIRED: '必須項目です。<関数>',
				INVALID_EMAIL: 'メールアドレスの形式が正しくありません。<関数>',
				INFO_SAVED: '保存しました。<関数>'
			});
		}
	};
})();

// Define your core UI functions here
function getConst() {
	// Core UI code goes here
	return Object.freeze({
				REQUIRED: '必須項目です。<関数>',
				INVALID_EMAIL: 'メールアドレスの形式が正しくありません。<関数>',
				INFO_SAVED: '保存しました。<関数>'
	});
}

ClientScriptからの呼び出しはこうなります。

OnLoadCallUiScriptTest (ClientScript)
+    var scriptName2 = 'x_1603370_uiscri_0.AppConstants2';

    if (typeof g_ui_scripts !== 'undefined') {
        // Workspace(WEP)
        g_ui_scripts.getUIScript(scriptName2).then(function(result) {
            console.log(result);
+    		const constObj = result.getConst();
+           g_form.addInfoMessage(JSON.stringify(constObj));
+    		g_form.addInfoMessage(constObj.INVALID_EMAIL);

        });
    } else {
        // クラシックUI
        ScriptLoader.getScripts(scriptName2 + '.jsdbx', function() {
+    		const constObj = getConst();
+           g_form.addInfoMessage(JSON.stringify(constObj));
+    		g_form.addInfoMessage(constObj.INVALID_EMAIL);
        });
    }

image.png

image.png

これでどちらも呼び出せている事を確認できます。
関数を返すのが想定だとするとこういう形でも良さそうですね。

複数の関数を返すUIScriptを呼び出してみる

次に複数の関数を定義したUIScriptを呼び出してみます。
こういう形が標準的なのかなと思います。

適当に計算関数を定義したUIScriptを作成します。

AppCalcLib (UIScript)
// Replace "yourFunctionName" with an appropriate function name
(function() {
    return {
        // Define your Workspace functions here
        add: function(a, b) {
            return a + b;
        },

        sub: function(a, b) {
            return a - b;
        },

        mult: function(a, b) {
            return a * b;
        },

        div: function(a, b) {
            return a / b;
        }
    };
})();

// Define your core UI functions here
function add(a, b) {
    return a + b;
}

function sub(a, b) {
    return a - b;
}

function mult(a, b) {
    return a * b;
}

function div(a, b) {
    return a / b;
}

上記を呼び出してみます。

OnLoadCallUiScriptTest (ClientScript)
+    var scriptName3 = 'x_1603370_uiscri_0.AppCalcLib';
	
	if (typeof g_ui_scripts !== 'undefined') {
		// Workspace(WEP)
		g_ui_scripts.getUIScript(scriptName3).then(function(result) {
			console.log(result);
+			g_form.addInfoMessage(result.add(1,2));
+			g_form.addInfoMessage(result.sub(1,2));
+			g_form.addInfoMessage(result.mult(1,2));
+			g_form.addInfoMessage(result.div(1,2));
		});
	} else {
		// クラシックUI
		ScriptLoader.getScripts(scriptName3 + '.jsdbx', function() {
+			g_form.addInfoMessage(add(1,2)); // グローバルを直接参照
+			g_form.addInfoMessage(sub(1,2)); // グローバルを直接参照
+			g_form.addInfoMessage(mult(1,2)); // グローバルを直接参照
+			g_form.addInfoMessage(div(1,2)); // グローバルを直接参照
		});
	}

image.png

image.png

全ての関数が利用出来て動作も問題なさそうです。

複数のUIScriptを読み込む

ここまでで3つのUIScriptを作成しました。
検証としては1つ1つ試していましたが、実際には複数のUIScriptを呼び出して使いたいケースがほとんどだと思います。
1つ1つ試していたので実際には不要個所をコメントアウトして利用していたのですが、下記のコードで複数呼び出すを試してみます。

複数のUIScriptを読み込むClientScript
OnLoadCallUiScriptTest (ClientScript)
function onLoad() {
   //Type appropriate comment here, and begin script below
   var scriptName = 'x_1603370_uiscri_0.AppConstants';

    if (typeof g_ui_scripts !== 'undefined') {
        // Workspace(WEP)
        g_ui_scripts.getUIScript(scriptName).then(function(result) {
            console.log(result);
            g_form.addInfoMessage(result.AppConstants.INVALID_EMAIL);
        });
    } else {
        // クラシックUI
        ScriptLoader.getScripts(scriptName + '.jsdbx', function() {
            console.log(AppConstants);
            g_form.addInfoMessage(AppConstants.INVALID_EMAIL); // グローバルを直接参照
        });
    }

	var scriptName2 = 'x_1603370_uiscri_0.AppConstants2';

    if (typeof g_ui_scripts !== 'undefined') {
        // Workspace(WEP)
        g_ui_scripts.getUIScript(scriptName2).then(function(result) {
            console.log(result);
    		const constObj = result.getConst();
            g_form.addInfoMessage(JSON.stringify(constObj));
    		g_form.addInfoMessage(constObj.INVALID_EMAIL);

        });
    } else {
        // クラシックUI
        ScriptLoader.getScripts(scriptName2 + '.jsdbx', function() {
    		const constObj = getConst();
            g_form.addInfoMessage(JSON.stringify(constObj));
    		g_form.addInfoMessage(constObj.INVALID_EMAIL);
        });
    }

	var scriptName3 = 'x_1603370_uiscri_0.AppCalcLib';
	
	if (typeof g_ui_scripts !== 'undefined') {
		// Workspace(WEP)
		g_ui_scripts.getUIScript(scriptName3).then(function(result) {
			console.log(result);
			g_form.addInfoMessage(result.add(1,2));
			g_form.addInfoMessage(result.sub(1,2));
			g_form.addInfoMessage(result.mult(1,2));
			g_form.addInfoMessage(result.div(1,2));
		});
	} else {
		// クラシックUI
		ScriptLoader.getScripts(scriptName3 + '.jsdbx', function() {
			g_form.addInfoMessage(add(1,2)); // グローバルを直接参照
			g_form.addInfoMessage(sub(1,2)); // グローバルを直接参照
			g_form.addInfoMessage(mult(1,2)); // グローバルを直接参照
			g_form.addInfoMessage(div(1,2)); // グローバルを直接参照
		});
	}
	
}

一応これでも動作はしますが可読性も悪く、特にWorkspaceだと複合的に使いづらいので解決したいところです。
image.png

image.png

解決策の1つがHow to make UI Scripts work in all User Interfacesに載っていました。
Chain loadingの章になります。

少しだけコードを修正していますが、やっている事は同じです。
ハイライト部分のような定義にしておくことでWorkspaceではどのUIScriptを参照しているか分かりやすくなるのかなと思います。
allScriptsLoaded関数を呼ぶようにしていますが、なくてもloadNextの処理が終われば、UIScriptの関数は参照できます。

OnLoadCallUiScriptTest (ClientScript)
    var scriptName = 'x_1603370_uiscri_0.AppConstants';
    var scriptName2 = 'x_1603370_uiscri_0.AppConstants2';
    var scriptName3 = 'x_1603370_uiscri_0.AppCalcLib';
    
+    var scripts = {
+        constants: scriptName,
+        constants2: scriptName2,
+        calc: scriptName3
+    };

    var libs = {};

	/**
	 * 複数のUIScriptを読み込む再帰関数
	 */
    function loadNext(keys) {

        if (!keys.length) {
            allScriptsLoaded();
            return;
        }

        var key = keys.shift();
        var scriptName = scripts[key];

        if (typeof ScriptLoader !== 'undefined') {
			ScriptLoader.getScripts(scriptName + '.jsdbx', function() {
				console.log(scriptName + ' loaded');
                loadNext(keys);
			});
        } else {
            g_ui_scripts.getUIScript(scriptName).then(function(result) {

                console.log(scriptName + ' loaded');
                libs[key] = result;

                loadNext(keys);
            });
        }
    }

	/**
	 * すべてのUIScriptを読み込んだ際に呼ばれる関数
	 */
    function allScriptsLoaded() {
		if (typeof ScriptLoader !== 'undefined') {
			// Core UI
			g_form.addInfoMessage(AppConstants.INVALID_EMAIL);
			g_form.addInfoMessage(add(1, 2));
		}
		else {
			// WEP
			console.log(libs);
+ 			g_form.addInfoMessage(libs.constants.AppConstants.INVALID_EMAIL);
+ 			g_form.addInfoMessage(libs.calc.add(1, 2));
		}
    }

    loadNext(Object.keys(scripts));

image.png

image.png

各UIScriptが呼び出しできている事を確認できました。

別の書き方で複数のUIScriptを読み込んでみる

上記のやり方で一度に複数のUIScriptを読み込める事は確認できましたが、個人的にちょっと難しい気もするので他の書き方でも試してみました。
こっちの方がちょっとコード量少なくて済むのかなと思います。

OnLoadCallUiScriptTest (ClientScript)
var scriptName = 'x_1603370_uiscri_0.AppConstants';
var scriptName2 = 'x_1603370_uiscri_0.AppConstants2';
var scriptName3 = 'x_1603370_uiscri_0.AppCalcLib';

var scripts = [
        scriptName,
        scriptName2,
        scriptName3
    ];

    if (typeof g_ui_scripts !== 'undefined') {
        // Workspace
+        Promise.all(
+            scripts.map(function(name) {
+                return g_ui_scripts.getUIScript(name);
+            })
        ).then(function(results) {
			var constants = results[0];
			var constants2 = results[1];
			var calc = results[2];

			g_form.addInfoMessage(constants.AppConstants.INVALID_EMAIL);
			g_form.addInfoMessage(calc.add(1, 2));
		});
    } else {
        // Classic
		const classicScripts = scripts.map(script => script + '.jsdbx');
		ScriptLoader.getScripts(classicScripts, function() {
			g_form.addInfoMessage(AppConstants.INVALID_EMAIL);
        	g_form.addInfoMessage(add(1, 2));
		});
    }

g_ui_scripts.getUIScriptは非同期関数なのでPromise.allで一気に読み込む形ですね。ScriptLoader.getScriptsに関しては配列を渡してあげると一気に読み込んでくれるのでこういう書き方もありかなと個人的には思いました。

終わりに

冒頭にもあった通りClientScriptを書いているとハードコーディングが増えてしまっていたのでその解決策としてUIScriptの呼び出しを検証してみました。
ServiceNowとしては10行以上のコードを書かない事を推奨?してるとかだったと思います。こういうのもアンチパターン、ServiceNowの思想から外れてしまうのかもしれないですが、どなたかの参考になれば幸いです。
ServiceNowについてはまだまだ触り始めたばかりですので、誤りやご指摘等あればお願い致します。m(_ _)m

Appendix:UIScriptで同じ関数を定義していたらどうなるか?

ちなみに複数のUIScriptを読めるようになったのですが、同じ関数名が定義されているどういう挙動になるのかも少し検証してみました。
でたらめな計算するUIScriptを他のUIScriptと同じ関数名で定義してみます。

AppCalcLib (UIScript)
// Replace "yourFunctionName" with an appropriate function name
(function() {
    return {
        // Define your Workspace functions here
        add: function(a, b) {
            return a + b;
        },

        sub: function(a, b) {
            return a - b;
        },

        mult: function(a, b) {
            return a * b;
        },

        div: function(a, b) {
            return a / b;
        }
    };
})();

// Define your core UI functions here
function add(a, b) {
    return a + b;
}

function sub(a, b) {
    return a - b;
}

function mult(a, b) {
    return a * b;
}

function div(a, b) {
    return a / b;
}
AppCalcLibWrong (UIScript)
// Replace "yourFunctionName" with an appropriate function name
(function() {
    return {
        // Define your Workspace functions here
        add: function(a, b) {
+            return a * b;
        },

        sub: function(a, b) {
+            return a * b;
        },

        mult: function(a, b) {
            return a * b;
        },

        div: function(a, b) {
+            return a * b;
        }
    };
})();

// Define your core UI functions here
function add(a, b) {
+    return a * b;
}

function sub(a, b) {
+    return a * b;
}

function mult(a, b) {
    return a * b;
}

function div(a, b) {
+    return a * b;
}

上記を一気に読み込んでみます。

OnLoadCallUiScriptTest (ClientScript)
    var scriptName = 'x_1603370_uiscri_0.AppConstants';
    var scriptName2 = 'x_1603370_uiscri_0.AppConstants2';
    var scriptName3 = 'x_1603370_uiscri_0.AppCalcLib';
+   var scriptName4 = 'x_1603370_uiscri_0.AppCalcLibWrong';

	var scripts = [
        scriptName,
        scriptName2,
        scriptName3,
+		scriptName4
    ];

    if (typeof g_ui_scripts !== 'undefined') {
        // Workspace
        Promise.all(
            scripts.map(function(name) {
                return g_ui_scripts.getUIScript(name);
            })
        ).then(function(results) {
			var constants = results[0];
			var constants2 = results[1];
			var calc = results[2];
			var calcWrong = results[3];

+			g_form.addInfoMessage(calc.add(1, 2));
+			g_form.addInfoMessage(calcWrong.add(1, 2));
		});
    } else {
        // Classic
		const classicScripts = scripts.map(script => script + '.jsdbx');
		ScriptLoader.getScripts(classicScripts, function() {
+        	g_form.addInfoMessage(add(1, 2));
		});
    }

image.png
Workspaceはこうなりました。
別々のScriptとして読み込めているので同じ関数名でも識別はできそうです。

image.png
Core UIの方ですが、globalに読み込まれる関係上
同じ関数名だと後から読み込んだもので定義が上書きされるようです。
本来は3を返してほしい所ですが、掛け算となり2が表示されてしまっているようです。
もしかするとですがwindow関数とかも上書きされてしまうのでしょうか。
UIScriptを使う場合は関数名が被らないようにユニークなものにしておく必要がありそうです。

参考URL

以下のURLを参考にしました。有益なサイト達に感謝します。
UI Script を Client Script から呼び出す

How to make UI Scripts work in all User Interfaces

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?