More client server code sharing

In Sharing code between client and server I wrote about how you could write code that could be used by both your client JavaScript and your server Apps Script, so you could pool utility functions when writing Add-ons and HTMLService and also manage all your code in the Apps Script IDE GS editor. 

Then in Namespaces in libraries and scripts, I showed how you could encapsulate code in its own namespace. Together, these two techniques make a powerful combination to help you avoid rewriting stuff and making global namespace errors. But you have to watch out for a few things. 

Recap

The key to Sharing code between client and server is this server side code. 

/**
* given an array of .gs file names, it will get the source and return them concatenated for insertion into htmlservice
* like this you can share the same code between client and server side, and use the Apps Script IDE to manage your js code
* @param {string[]} scripts the names of all the scripts needed
* @return {string} the code inside script tags
*/

function requireGs (scripts) {
    return '<script>\n' + scripts.map (function (d) {
        return ScriptApp.getResource(d).getDataAsString();
    })
    .join('\n\n') + '</script>\n';
}

paired with this in your client Html template

<?!= requireGs (['usefulThings' , 'clientJs']); ?>

This will bring in any code in usefulThings.gs and clientJs.gs - normal Apps Script files that you can use server side if you want - into your client side script which will then be available for client side use by HTML service.

Using script manifests

This version of requireGs allows the client to request any Apps Script file. It may be that you want to lock that down (for example if this project is also being used as a library you may not want to expose a function that can pick up any script file), or maybe you simply prefer to manage access to script source on the server side. 

Here is an alternative version where you can manage the scripts authorized for client side execution from the server.

The client requests a set of scripts by referencing a manifest name. requireGsManifest() on the server knows which scripts make up that manifest.
/**
* given a manifest name it will get the source and return them concatenated for insertion into htmlservice
* like this you can share the same code between client and server side, and use the Apps Script IDE to manage your js code
* @param {string} manifest the manifest name
* @return {string} the code inside script tags
*/
function requireGsManifest (manifest) {

  var manifests = [
    { name: 'sankeysnip',
      scripts: ['Utils', 'Sankey' , 'Shared' , 'View' ,'Home' ,'App', 'Client']
    }
  ];
  
  // find requested manifest
  var target = manifests.filter (function (d) {
    return d.name === manifest
  });
  
  // should be exactly 1
  if(target.length !== 1) { 
    throw 'manifest ambiguous ' +manifest;
  }
  
  // get all the script contents
  return '<script>\n' + target[0].scripts.map (function (d) {
    return ScriptApp.getResource(d).getDataAsString();
  })
  .join('\n\n') + '</script>\n';

}

In the client html - 
<?!= requireGsManifest('sankeysnip');  ?>

Restrictions

You can have any kind of JavaScript in your feeder Apps Script files - even ones that access the DOM,  or other objects that don't exist in Apps Script - just so long as Apps Script does not attempt to execute them.

So  this can live perfectly happily within the App Script IDE as a .gs file

function foo () {
    console.log('im not executed');
    var d = document.getElementById('bar');
}

but this outside a function will cause an error - which is not a bad thing actually, since execution of things in global space often leads to unexpected behavior.

var d = document.getElementById('bar');

RequireJs

I also use a similar method to pick up pure js that I've decided to maintain in HTML files (as opposed to GS) files. Like requireGs, there's both a manifest and list version. 

/**
* given an array of .html file names, it will get the source and return them concatenated for insertion into htmlservice
* @param {string[]} scripts the names of all the scripts needed
* @return {string} the code inside script tags
*/
function requireJs (scripts) {
    return '<script>\n' + scripts.map (function (d) {
        return HtmlService.createHtmlOutputFromFile(d+".js").getContent();
    })
    .join('\n\n') + '</script>\n';
}
/**
* given a manifest name it will get the source and return them concatenated for insertion into htmlservice
* @param {string} manifest the manifest name
* @return {string} the code inside script tags
*/
function requireJsManifest (manifest) {

  // find requested manifest
  var target = manifests.filter (function (d) {
    return d.name === manifest
  });
  
  // should be exactly 1
  if(target.length !== 1) { 
    throw 'manifest ambigous ' +manifest;
  }
  
  // get all the script contents
  return '<script>\n' + target[0].scripts.map (function (d) {
    return HtmlService.createHtmlOutputFromFile(d + ".js").getContent();
  })
  .join('\n\n') + '</script>\n';

}


In my index.html, I call them like this


<?!= requireGsManifest('sankeysnip'); ?>
<?!= requireJsManifest('js'); ?>
And my manifests look like this

var manifests = [
  { name: 'sankeysnip',
   scripts: ['Utils', 'Sankey' , 'Shared' , 'View' ,'Home' ,'App', 'Client']
  },
  { name: 'js',
    scripts:['main']
  }
];

Namespaces

As discussed in Namespaces in libraries and scripts, encapsulating logically separate pieces of code is a pretty good thing to do. It also helps to enable avoiding some of the pitfalls of embedding executable JS code in the GS IDE. 

To recap, you can create a namespace like this - shown here with some  variables function that you might otherwise have declared globally

var Foo = (function() {
    'use strict';
    // this is namespace object
    var foo = {};

    // set a local property
    foo.name = 'foo';

    // a function in this namespace
    foo.showName = function () {
        Logger.log('namespace ' + foo.name);
    };

    return foo;
})();

However, using this pattern (function() {...})(); executes the enclosed code, so the content of you namespace (just like the global content) should not be executable if you are combining this with the technique for Sharing code between client and server.

so this is fine,

var Foo = (function() {
  var foo = {};
  foo.name = "bar";
  return foo;
})();

but this is not, since Apps Script would try to execute something using the document object which only exists client side.

var Foo = (function() {
  var foo = {};
  foo.name = document.getElementById("bar").id;
  return foo;
})();

Initialize

You can get round this by equipping namespaces like this with an initialization function in which you embed any code that would otherwise execute

var Foo = (function() {
  var foo = {};
  foo.initialize = function () {
    foo.name = document.getElementById("bar").id;
  }
  return foo;
})();

and then when you are ready to go, 

Foo.initialize();

The gadget spec URL could not be found
The gadget spec URL could not be found


The gadget spec URL could not be found

Example

The benefits of  this approach is that you can have code that can execute perfectly happily on both client and server containing Vanilla JavaScript living alongside in the Apps Script IDE, but separated from, code that is specific for either the Server or the Client.

Here's an example from one of my add-ins

My html content 

I'm picking up all these script files from the Apps Script IDE, some of which I use both server and client side

<?!= requireGs (['Utils' , 'Sankey' , 'Shared' , 'View' ,'Home' ,'App', 'Client']); ?>
<?!= requireJs (['main']); ?>

and this is my main.js - the only Apps Script HTML.js file.

    

    window.onload = function () {

        // set up client app structure
        App.initialize();

        // initialize this page
        Home.initialize();

        // get some data to the client and start
        Client.getData();

    };


For more like this, see  Google Apps Scripts snippets. Why not join our cummunity , follow the blogtwitterG+  .

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, available All formats are available now from O'Reilly,Amazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.




Comments