Modern Ajax Calls
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎06-06-2023 04:54 AM - edited ‎06-06-2023 06:16 AM
ServiceNow - Modern Ajax Calls
Introduction
TLDR
In this article, I show how to implement a more modern way to create Ajax calls in ServiceNow scripts. This is a proposed way to create new Script Includes and not an official best practice. I am currently experimenting myself with the best way to organize my code, and would love to hear your feedback on this.
With ServiceNow's Tokyo release, we finally got support for modern javascript. This opens the door for many exciting new possibilities, and also is a good opportunity to improve upon some of the outdated patterns that we have been using for years (often without really thinking about it).
In this article I focus on a way on how to write Ajax script includes in a modern style, using modern javascript syntax. Under the hood nothing changes - but the code looks much nicer, and is easier to read and maintain.
You might not know this, but ServiceNow was not only very late in adopting ES6+ (admittedly for good technical reasons), but the same story had already happened with ES5. Only in 2016 with the Helsinki release did we get ES5 support - 7 years late to the party.
What's worse, under the hood ServiceNow continued to use ES3 style patterns in many places - and continues to do so today, even in Tokyo and later releases! One of these cases is the famous GlideAjax pattern. Let's examine what we can do to change that.
The Promised Land
Let's start with the end in mind - what would a modern client-server communication pattern look like, if we had free reign? I propose something like the following.
The Script Include
Our Script Include should just be a simple class, with some public methods. We can used named parameters (using modern destructuring syntax), and we return a plain old JavaScript value (of course this could also be an instance of another class, if we are dealing with more complicated payloads).
MyAjaxScriptInclude = class {
getGreeting({formal = false, includePhrase = true}) {
const salutation = formal
? `Dear Mr/Ms/Mrs ${gs.getUser().lastName}.`
: `Hi ${gs.getUser().firstName}.`;
const phrase = includePhrase
? ' How are you today?'
: '';
return salutation + phrase;
}
};
This is simple enough - a class with a single method (might as well be static) that constructs a short greeting for the user. Unfortunately, ServiceNow does not store the salutation, but that shall not bother us here. Note how we can use template literals to make the code more readable (once you are used to that sort of style).
The Client Side Code
On the client, we just want to call our script include without having to worry about the details. Let's imagine something like this in our client side code (it could be a client script, client UI action or anything else that runs in the browser):
async function someFunction() {
console.log("Let's call the server...");
const result = await MyAjaxScriptInclude.myFn({formal : true, includePhrase : true});
console.log('...and this is the result: ', result);
}
someFunction();
Very nice. We need to declare the function as async to be able to use async/await, but then it looks just like synchronous code. All the complexity is hidden from the caller, and we can simply call the function as if it was directly handled on the client.
Of course there are some missing pieces here to make this work - but we will get there in the end. I am also skipping over some of the typical boiler code you would usually add to make the example more maintainable. In particular, I do not add any error handling, or documentation, which I would normally do.
So, now that you have seen where we want to go, let's see how we get there. We will start with some history.
ES3 and Prototype
In ancient times, GlideAjax was even clunkier than today - ServiceNow actually recommended to manually modify and parse the XML response if you wanted to return more than a simple string (just check the old wiki https://web.archive.org/web/20171201033835/http://wiki.servicenow.com/index.php?title=GlideAjax#Retu...).
This got a bit easier in with the introduction of getXMLAnswer() (and using JSON.encode / JSON.decode), and the following pattern is probably the most common one ever since.
The Script Include
This is what our script include would normally look like:
var MyAjaxScriptInclude = Class.create();
MyAjaxScriptInclude.prototype = Object.extendsObject(
global.AbstractAjaxProcessor,
{
getGreeting: function() {
var formal = this.getParameter("sysparm_formal");
var includePhrase = this.getParameter("sysparm_includePhrase");
var salutation = formal
? "Dear Mr/Ms/Mrs " + gs.getUser().lastName + "."
: "Hi " + gs.getUser().firstName + ".";
var phrase = includePhrase ? " How are you today?" : "";
return salutation + phrase;
},
type: "MyAjaxScriptInclude",
}
);
Notice the boiler plate code using the custom functions Class.create() and Object.extendsObject(). This is the way we have been doing it for years, and it is not very pretty. But it works, I guess.
Notice also that the actual method does not publicly exhibit its signature - it is not clear what parameters it expects. Not what we wish from a modern API.
The Client Side Code
And this is how we would call it from the client:
console.log("Let's call the server...");
var ga = new GlideAjax("my_scope.MyAjaxScriptInclude");
ga.addParam("sysparm_name", "getGreeting");
ga.addParam("sysparm_formal", true);
ga.addParam("sysparm_includePhrase", true);
ga.getXMLAnswer(function (answer) {
console.log("...and this is the result:", answer);
});
This looks very old-school. We have to create a new GlideAjax object, and then add parameters to it using the addParam() method. We also have to pass a callback function (to be precise: closure) to the getXMLAnswer() method, which is called when the server responds (this is called the continuation-passing-style or CPS). The callback function can be declared inline or as a separate function, but it is still a bit of a mess, especially when we get into nesting functions inside functions.
global.Class[1]
Most people probably don't think too deeply about the above pattern and treat it as just some clerical duty to follow. But there is a lot of complexity hidden in the background, and it is worth understanding what is going on.
First, you might not suspect it, but Class.create() is actually not idiomatic JavaScript - it is a custom script include itself, stored in global.Class, with the following content:
gs.include("PrototypeServer");
Of course this does not actually tell you much, except that the actual logic you are looking for is in another script include, called PrototypeServer.
The PrototypeServer
I will not show the full content of the global.PrototypeServer script include here, you can take a look at that yourself.
Prototype was a javascript library that was new and popular around the time ServiceNow was being developed, and had some nice utility features that were quite useful, especially before ES5 came out in 2009. It fell out of favor when jQuery came along (if you still remember that one.
Simply speaking, the PrototypeServer script include adds a very reduced version of Prototype library with the custom "Class" factory method to the global object. It also patches the custom Object.extendsObject() method (and other methods) to the Object constructor. Ajax script includes then use this method to "extend" the AbstractAjaxProcessor class. It is a roundabout way of implementing inheritance, and just adds unnecessary complexity to the code without any real benefit. Typically you don't have to worry about that, but this kind of stuff bothers me, so I try to simplify it as much as possible.
See: http://prototypejs.org/learn/class-inheritance.html
This was the way before ES5 came around. For some reason, ServiceNow never got rid of that. Even now in Tokyo, it is still used. The Prototype library is now deprecated and history, but kept alive by thousands of ServiceNow instances. In my opinion, this should have been changed back in 2009 when ES5 came out.
If you want to learn more about what ServiceNow is doing behind the scenes, you can take a look at the AbstractAjaxProcessor class that handles part of the method invocation boiler plate.
2009 - Using ES5 Classes
In 2009, ES5 was introduced. Now technically, ServiceNow followed suit only in 2016, but in theory, the following code should have been possible back then.
With ES5, we get Object.create(), which allows us to easily create a new object that inherits from another object. This is slightly different from the custom Object.extendsObject() that ServiceNow uses, in that it uses the prototype chain to inherit from the parent object, rather than copying the parent object’s properties into the child object. I do not see any reason why cloning the parent would be better, so let's not do that. The following code shows how to use Object.create().
Server Side
function MyAjaxTestScript(request, responseXML, gc) {
this.initialize.call(this, request, responseXML, gc);
}
MyAjaxTestScript.prototype = Object.create(global.AbstractAjaxProcessor.prototype);
MyAjaxTestScript.prototype.myFn = function() {
const myPar1 = this.getParameter('sysparm_parm1');
return 'value of parm1: ' + myPar1;
};
Note that Object.create here has nearly the same effect as the custom 'Object.extendsObject', but does not need any custom library code.
Also, we do not need the custom Class.create method to create a class - defining a constructor function and a prototype object that inherits from the base class is all we need.
The downside is we need to explicitly call initialize, since that would normally be handled by Class.create as well.
Client Side
The client side technically does not have to change a lot.
const ga = new GlideAjax('my_scope.MyAjaxTestScript');
ga.addParam('sysparm_name', 'myFn');
ga.addParam('sysparm_parm1', 'p1');
ga.getXMLAnswer(function(a) { console.log('result: ' + a);});
Note that getXMLAnswer automatically hands us the XML response as a string. All too often I see people still using getXML and then parsing the XML response manually. This is not necessary, and makes the code more complex than it needs to be.
Using ES6 Classes
In ES6, we can use the class keyword to create a new class. This is pretty much doing the same thing as we did before, just with different syntax - one which people coming from other languages might find more familiar. But behind the scenes, it is doing the same thing as the ES5 example above, so ServiceNow does not mind. To JavaScript, a class is still just a function.
Updated Server Side Code
This is how you do it:
MyAjaxTestScript2 = class extends global.AbstractAjaxProcessor {
constructor(request, responseXML, gc) {
super(request, responseXML, gc);
}
myFn() {
const myPar1 = this.getParameter('sysparm_parm1');
return 'value of parm1: ' + myPar1;
}
};
The client side code does not need to change at all.
Using Promises
Now let's get to the client - we want to move away from callbacks and use promises. The following looks longer and more complicated, and if you are not familiar with Promises, I am sorry that this might be quite a complicated concept to understand. But async/await (see below) make this much clearer.
function callFn() {
return new Promise((resolve, reject) => {
const ga = new GlideAjax("my_scope.MyAjaxTestScript2");
ga.addParam("sysparm_name", "myFn");
ga.addParam("sysparm_parm1", "p1");
ga.getXMLAnswer(resolve);
});
}
callFn().then((result) => {
console.log("result: ", result);
});
The above code is specific for our defined API, but we can make it a bit more generic:
function callFn(si, method, args) {
return new Promise((resolve, reject) => {
const ga = new GlideAjax(si);
ga.addParam("sysparm_name", method);
ga.addParam("sysparm_parm1", JSON.stringify(args));
ga.getXMLAnswer(resolve);
});
}
callFn("my_scope.MyAjaxTestScript2", "myFn", "p1").then((result) => {
console.log("result: ", result);
});
This way, our function callFn does not depend on the specific remote method API and we can freely reuse it. The method invocation now contains all the information we need to resolve the call.
Note that while we still use the class pattern on the server side (for legacy reasons, to be fair), on the client this does not make sense. By default, the Server Side Script Includes do not keep state (even though it would not be impossible to implement that, but if that is a good idea is another topic). So we can just use a plain function on the client side, or other static patterns such as namespaces, abstract classes, etc.
Async / Await
Now that we have Promises, we can use the async/await pattern to make our code more readable.
async function someFunction() {
console.log("let's call the server");
const result = await callFn('my_scope.MyAjaxTestScript2', 'myFn', 'p1');
console.log('and here we go: ', result);
}
someFunction();
In this example, we put all our code into a function marked async, so we can use the new syntactic sugar. The little keyword await is all we need to make sure the system will call our function asynchronously and subsequent code will not block - it's like magic. Again, the magic is behind the curtains, and it still works the same ways as with CPS or promises, you just have to read a bit between the lines now.
I am personally a huge fan of async/await - it allows me to really focus on the business logic without having to think about which part runs synchronously or asynchronously. This helps when maintaining my code later on. Nested CPS is just a nightmare - if you ever tried to implement multiple subsequent Ajax calls in ServiceNow, for example if you want to validate a client transaction before submitting the form WITHOUT blocking the the browser, you know what I mean.
Further Topics
The above snippets work, but are not quite enterprise ready yet - more a proof of concept. I hope it can serve as a good starting point for a discussion. In the future, I plan on creating a working framework to generate the necessary boiler plate code automatically, but if you want to use this pattern already, you can start with the code above.
There are other considerations that are not addressed yet (How do we test the code? Can we make it type safe? Can we make it generic?). I am curious to hear your thoughts on this, and am looking forward to see more modern code in ServiceNow scripts.
This and the next chapter are a bit of an excursion. Feel free to skip ahead to # 2009 - Using ES5 Classes if you are not interested in the technicalities behind ServiceNow script includes. ↩︎
- 1,956 Views
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
‎08-15-2023 05:24 AM
Update after some lessons learned:
If you have more than one class you want to call from the client, you will want a way to encapsulate the logic responsible to handle the request. You will also want to have a robust and transparent way to deal with exceptions that can occur on the server (my personal preference is to see exceptions that are thrown on the server during an ajax call directly in the client - this makes debugging a lot easier, in my opinion).
this.AjaxClass = class extends global.AbstractAjaxProcessor {
/**
* This generic endpoint accepts a method name and arguments as JSON string,
* and calls the method with the arguments. It will package the result in a JSON string
* and return it.
* Any exception will be caught and returned as error, so it can be handled in the calling code.
* If the method returns undefined, the result will be null.
*
* The result object will have the following structure:
* {
* status: 'success' | 'error',
* returnValue: <JSON string of return value or null>,
* message: <error message>,
* exception: <error details>
* }
*
* @returns {string} JSON string with result object
*/
endPoint() {
const method = this.getParameter('sysparm_method');
const args = this.getParameter('sysparm_arguments');
const result = {
status: 'error',
returnValue: null,
};
let argObj;
try {
argObj = JSON.parse(args);
} catch (e) {
result.message = 'Could not parse arguments.';
return JSON.stringify(result);
}
// try to find method, or static function, either one works
const fn = this[method] || this.constructor[method];
if (typeof fn !== 'function') {
result.message = `Method ${method} not found.`;
return JSON.stringify(result);
}
let returnObj;
try {
returnObj = fn(argObj);
} catch (e) {
result.message = 'Method invocation failed.';
result.exception = this.constructor.serializeException(e);
return JSON.stringify(result);
}
try {
if (typeof returnObj === 'undefined') {
result.returnValue = null;
} else {
result.returnValue = JSON.stringify(returnObj);
}
} catch (e) {
result.message = 'Method result could not converted to JSON.';
result.exception = JSON.stringify(e, Object.getOwnPropertyNames(e));
return JSON.stringify(result);
}
result.status = 'success';
return JSON.stringify(result);
}
/**
* Converts the exception generated by ServiceNow into a string
* that can be parsed on the client.
*
* @Param {Error} ex exception object to serialize
* @returns {string} JSON string of exception object
*/
static serializeException(ex) {
const exObj = {};
Object.getOwnPropertyNames(ex).forEach((key) => {
exObj[key] = ex[key];
});
// rhinoException is a Java object, so we need to force string conversion
if ('rhinoException' in ex) {
exObj.rhinoException = String(ex.rhinoException);
}
return JSON.stringify(exObj);
}
};
As a counterpoint on the client, you will want a unified way of invoking the server and unwrapping the result. You also want to see what exceptions were thrown on the server - my preference here is to simply rethrow them client side. The following UI script achieves this:
/* exported ajaxCall */
/**
* Calls a method on a server script include and returns the result (as promise).
* Uses the custom 'AjaxClass' script include. If the invoked method returns undefined,
* the promise will resolve with null.
* If the invoked method throws an exception, the promise will reject with an error.
*
* @Param {string} si Script include name (including scope)
* @Param {string} method Method name (name of function in called script include)
* @Param {object} args Arguments object (has to be serializable!)
* @returns {Promise} Promise that resolves with the result of the method call
*/
function ajaxCall(si, method, args) {
/**
* To rethrow exceptions on the client, we parse the passed in
* exception string and create a new error object from it.
* The error object will have the same properties as the original exception
* (although only one level of properties is supported).
*
* @Param {string} exceptionStr JSON string of exception object
* @returns {Error} Error object
*/
function getError(exceptionStr) {
const exObj = JSON.parse(exceptionStr);
const err = new Error();
err.source = exObj;
// set error properties from exception object
Object.entries(exObj).forEach(([key, value]) => {
err[key] = value;
});
return err;
}
let argJSON;
try {
argJSON = JSON.stringify(args);
} catch (e) {
const ex = new Error(`Ajax method call ${si}.${method} not possible - could not convert arguments to JSON.`);
ex.cause = e;
throw ex;
}
return new Promise((resolve, reject) => {
const ga = new GlideAjax(si);
ga.addParam('sysparm_name', 'endPoint');
ga.addParam('sysparm_method', method);
ga.addParam('sysparm_arguments', argJSON);
ga.getXMLAnswer((resultStr) => {
let resultObj;
try {
resultObj = JSON.parse(resultStr);
} catch (e) {
console.debug('JSON string was: ', resultStr);
reject(new Error('Could not parse ajax result of method call ' + si + '.' + method + '.'));
return;
}
if (resultObj.status !== 'success') {
if (resultObj.exception) {
const ex = new Error('Method call ' + si + '.' + method + ' returned error: ' + resultObj.message);
ex.cause = getError(resultObj.exception);
console.debug('Exception source: ', ex.cause.source);
if (ex.cause.source.stack) {
console.debug('Exception stack: ', ex.cause.source.stack);
}
reject(ex);
return;
}
reject(new Error('Method call ' + si + '.' + method + ' returned error: ' + resultObj.message));
return;
}
let returnVal;
try {
returnVal = JSON.parse(resultObj.returnValue);
} catch (e) {
console.debug('JSON string was: ', resultStr);
reject(new Error('Could not parse return value of method call ' + si + '.' + method));
return;
}
resolve(returnVal);
});
});
}
Now, you can very simply set up your own server side class as follows:
this.MyAjaxClass = class MyAjaxClass extends my_scope.AjaxClass {
static myFunction(myArguments) {
// some code
return someValue;
}
};
And to call it from the client:
const result = await ajaxCall('my_scope.MyAjaxClass', someArguments);