早速ですが問題です。Client callableなScript Includeの次のコードはうまく動きません。何が問題でしょう?なお、nullチェックをしていないのは簡単のためです。
// Client callable Script Include内の関数
serverFunc: function() {
var sysids = this.getParameter('sysparm_sysids'); // クライアント側からはカンマ区切りのSysIDを文字列で渡している
var sysid_list = sysids.split(','); // 文字列を分割してリスト化 ★ココが期待通り動かない
},
// 呼び出し側のクライアントサイドスクリプト
var ga = new GlideAjax('<Script Include name>');
ga.addParam('sysparm_name', 'serverFunc');
ga.addParam('sysparm_sysids', <SysIDをカンマ区切りで繋いだ文字列>);
ga.getXMLAnswer(callback, null, sysid);
答えは、Script IncludeにおけるgetParameter()の戻り値はJavaScriptのStringではなくJavaオブジェクトであるため、String()でJavaScriptのStringに変換してからでないとsplit()メソッドが想定通り動かないからです。
結論はこれだけなのですが、マニアに向けてもう少し深堀りして解説したいと思います。解決方法だけ知りたい方は下記のナレッジ記事をご覧ください。
KB0826476: Calling getParameter in GlideAjax returns an object and not a string
Script Includeの基本的な部分について知り方かった方はこちらをご覧ください。
Script IncludeのgetParameter()で取得できる引数はJavaのStringオブジェクト
上述の通り、Script IncludeにおけるgetParameter()の戻り値はJavaScriptのStringではなくJavaオブジェクトです。確かめてみます。
// Client callable Script Include内の関数
serverFunc: function() {
var arg1 = this.getParameter('sysparm_arg1'); // 引数を取得
var arg1_type = Object.prototype.toString.call(arg1);
gs.info("[Script Include Parameter Test] typeof arg1: " + typeof arg1);
gs.info("[Script Include Parameter Test] Object.prototype.toString.call(arg1): " + arg1_type);
gs.info("[Script Include Parameter Test] arg1: " + arg1);
gs.info("[Script Include Parameter Test] arg1.split(','): " + arg1.split(','));
},
var ga = new GlideAjax('global.MyClientCallableScriptInclude');
ga.addParam('sysparm_name', 'serverFunc');
ga.addParam('sysparm_arg1', 'string,from,client-side');
ga.getXMLAnswer(callback, null, null);
結果は次の通りです。まずわかるのは、こいつはJavaScriptの文字列ではなくJavaオブジェクトであるということです。さらに、文字列として出力すると正しく取得できるが、split()メソッドを呼び出すと、Javaオブジェクト自体を表す文字列が表示されてしまっています。当然ながらこれは期待した挙動ではありません。
[Script Include Parameter Test] typeof arg1: object
[Script Include Parameter Test] Object.prototype.toString.call(arg1): [object JavaObject]
[Script Include Parameter Test] arg1: string,from,client-side
[Script Include Parameter Test] arg1.split(','): [Ljava.lang.String;@67b3a6
普通に使う分には問題にならないことが多い
上述の例の通り、単純に文字列として使う分には特に問題になりません。ですので次に示すような「クライアントサイドからサーバーサイドにSysIDを渡し、サーバーサイドでSysIDからレコードを取得して処理する」というよくあるパターンではデータ型を意識することはないでしょう。
// こんな使い方なら、特に問題なし
serverFunc: function() {
var user_sysid = this.getParameter('sysparm_user_sysid'); // 引数を取得
var user = new GlideRecord('sys_user');
if (user.get(user_sysid)) {
// 処理
}
},
とはいえ、この記事ではより良い・正しいコードを書く方法を考えます。
Javaオブジェクト to JavaScriptのStringへの変換方法
getParameter()はJavaオブジェクトを返すので、正しいコードを書くためには、まずJavaScriptのStringに変換してあげなければなりません。これには2つの方法が考えられます。
String()を使う
下記のナレッジ記事でも紹介されている方法は、String()関数を使ってJavaScriptのStringに変換する方法です。実際にやってみます。
KB0826476: Calling getParameter in GlideAjax returns an object and not a string
serverFunc: function() {
var arg1 = this.getParameter('sysparm_arg1'); // 引数を取得
var arg1_jsstr = String(arg1);
var arg1_jsstr_type = Object.prototype.toString.call(arg1_jsstr);
gs.info("[Script Include Parameter Test] typeof arg1_jsstr: " + typeof arg1_jsstr);
gs.info("[Script Include Parameter Test] Object.prototype.toString.call(arg1_jsstr): " + arg1_jsstr_type);
gs.info("[Script Include Parameter Test] arg1_jsstr: " + arg1_jsstr);
gs.info("[Script Include Parameter Test] arg1_jsstr.split(''): " + arg1_jsstr.split(',').toString());
},
結果は次の通りです。無事JavaScriptのStringに変換されています。
[Script Include Parameter Test] typeof arg1_jsstr: string
[Script Include Parameter Test] Object.prototype.toString.call(arg1_jsstr): [object String]
[Script Include Parameter Test] arg1_jsstr: string,from,client-side
[Script Include Parameter Test] arg1_jsstr.split(''): string,from,client-side
「ハイハイ。じゃあgetParameter()するときは毎回String()で変換してから使えばいいのね」と言いたくなりますが、これだとまだ足りません。
// ダメな例
var arg1 = String(this.getParameter('sysparm_arg1'));
なぜかというと、引数が必須でない場合、まずは if (arg1) などとして引数の存在チェックを行うと思いますが、このとき先にString()を使うと、String()はnullを”null”という文字列に変換してしまうため、引数が存在しなかった場合(.getParameter()がnullを返した場合)でも、常に if (arg1) がtrueになってしまうのです。具体的なコードを示します。
// ダメな例
var arg1 = String(this.getParameter('sysparm_arg1')); // getParameter()がnullを返した場合でも"null"という有効な文字列になるため、
if (arg1) { // ここが常にtrueになってしまう
// 引数が存在する場合の処理
}
ですので、より適切には次のようにすべきです。空文字列とnullを区別する必要がなければ、単に if (arg1) でもよいでしょう。
var arg1 = this.getParameter('sysparm_arg1');
if (arg1 != null) arg1 = String(arg1); // 存在チェックをしてから型変換
if (arg1) {
// 引数が存在(かつ空文字列ではない)する場合の処理
}
.getParameter()を2回呼ぶためオーバーヘッドはありますが、引数取得を1行で書くなら次のようになります。
var arg1 = this.getParameter('sysparm_arg1') != null ? String(this.getParameter('sysparm_arg1')) : null;
if (arg1) {
// 引数が存在する(かつ空文字列ではない)場合の処理
}
引数として”null”という文字列が来ることはまずないと思うので、その前提を置けるのであれば、次のようにしてもよいでしょう。前半の arg1 && は空文字列を考慮しています。
var arg1 = String(this.getParameter('sysparm_arg1'));
if (arg1 && arg1 != "null") {
// 引数が存在する(かつ空文字列ではない)場合の処理
}
(おまけ)j2jsを使う
マニア向けの記事ということで、KBではString()といわれているが、ServiceNow的な正解はこっちなのでは、と個人的に思っている方法も紹介しちゃいます。
ServiceNowの標準APIには、getParameter()以外にもJavaのオブジェクトを返す関数があります。このような場合に、JavaオブジェクトをJavaScriptのオブジェクトに変換するための関数を提供する”j2js”というScript Includeが標準で存在します。
使い方は簡単で、gs.include(“j2js”); でインクルードして、j2js()関数にJavaオブジェクトを渡すだけです。
serverFunc: function() {
gs.include("j2js");
var arg1 = this.getParameter('sysparm_arg1'); // 引数を取得
var arg1_jsstr = j2js(arg1); // 型変換
}
この関数はnullの考慮もされているため、String()と違いgetParameter()の戻り値をそのまま渡すことができます。
この関数は、API Referenceにも記載がある公式で利用が許容されている機能で、かつ無駄な関数呼び出しがなく1行で変換が可能なのですが、こいつにも欠点があり、j2js Script IncludeはGlobal scope onlyになってます。そのためScopedなScript Includeから使う場合は、GlobalのScript Includeを経由する必要があり、それはそれでめんどくさいという問題があります。そのため、紹介しておいてなんですが、後述のサンプルコードではString()を採用しています。
toString()は使わない
また、toString()を使えば、オーバーヘッドもなく、1行で次のように書けるのでは?と思った方がいるかもしれません。
var arg1 = this.getParameter('sysparm_arg1').toString();
しかし、Javaオブジェクトに対して.toString()を呼び出しても、JavaScriptのStringにはなりません。データ型がどうなるかを見てみましょう。
serverFunc: function() {
var arg1 = this.getParameter('sysparm_arg1'); // 引数を取得
var arg1_tostring = arg1.toString();
var arg1_tostring_type = Object.prototype.toString.call(arg1_tostring);
gs.info("[Script Include Parameter Test] typeof arg1_tostring: " + typeof arg1_tostring);
gs.info("[Script Include Parameter Test] Object.prototype.toString.call(arg1_tostring): " + arg1_tostring_type);
},
結果は次の通りです。
[Script Include Parameter Test] typeof arg1_tostring: object
[Script Include Parameter Test] Object.prototype.toString.call(arg1_tostring): [object JavaObject]
そのため、toString()は使えません。
getParameter()のサンプルコード
結論としてgetParameter()をどう使うべきかを表すサンプルコードを貼っておきます。下記のようにして取得すると、クライアントサイドで指定した引数の文字列(空文字列を含む)が正確にサーバーサイドに伝わり、かつクライアントサイドで引数を指定しない場合はnullになります。
serverFunc: function() {
// retrieve arguments
var arg1 = this.getParameter('sysparm_arg1');
if (arg1 != null) arg1 = String(arg1); // convert Java Object from getParameter() to JS String
var arg2 = this.getParameter('sysparm_arg2');
if (arg2 != null) arg2 = String(arg2); // convert Java Object from getParameter() to JS String
var arg3 = this.getParameter('sysparm_arg3');
if (arg3 != null) arg3 = String(arg3); // convert Java Object from getParameter() to JS String
// process with arg1, arg2, arg3
}
以上です。
コメント