auhtor_image
Volodymyr Terebus

Front End Development Practice Lead

Overview

We need to create a Library that any customer can easily integrate to their website.  The Library should provide a UI widget in the corner and have an API platform to intercommunicate, if a customer wants to override the default behavior.

 

Library Design

 

Common Rules for any JS Library

Here is a list of rules that each library should follow:

  1. Avoid overriding, changing, and modifying the global environment and the default CSS.  Avoid adding a method to global objects and changing the behavior of the DOM, as well.
  2. Avoid using a 3rd party libraries that can make changes in the environment, or conflict with another library version or other incompatible libraries.
  3. To prevent conflicts avoid exposing objects, with common names, to the global environment.
  4. Use unique names for all CSS class names and id selectors to prevent conflict.
  5. Library must be able to load in ES6 module, Common JS, Require JS, and Windows.
  6. When using JS, the library should load using either script tag, defer/async attributes or asynchronously.
  7. Make the library as small as possible.
  8. For further use, it is good to embed the library version inside the actual library.

 

This is how it will be

 

Let’s see how we did in following the rules

 

Firebase will be the only 3rd party library that we will use, considering it should be already in use. Firebase will not override any object in global environment and only exposes one global object – “firebase”. There may be conflicts if a customer’s website already uses firebase for personal reasons.

  • Will handle each case if website will load firebase library in its own way

We won’t use any polyfills, shims, and CSS normalizers. We will have a defined CSS and have a few fields exposed to the global environment.

  • Use a unique prefix for all CSS selectors (id, class, and attribute)
  • Use a unique prefix as well for all fields in the global environment

Our library should only work in the browser environment and use a browser feature that works with all browser versions. If not able to, use polyfills that will not expose itself to the global environment. Script in final stage should be in ES5.

Library is able to load using ES6 modules, script element, and Require JS

  • Use UMD

Library should be minimized

  • Use Uglify JS

Be able to debug library

  • A production (minimized) version with the ability to debug the library
  • Library version will be embedded within the library

Let’s see how integration will work for our library:

<!-- Step 1. Option 1 - to add by script tag -->
<!-- also you can remove 'async' and/or 'defer' attribute if want -->
<script type="text/javascript" async defer src="//chat.com/chat.widget.min.js"></script>

<!-- Step 1. Option 2 - to add by js in runtime -->
<script>
  (function () {
    var lc = document.createElement('script');
    lc.type = 'text/javascript';
    lc.async = true; // can remove if need
    lc.defer = true; // can remove if need
    lc.src = '//chat.com/chat.widget.js';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(lc, s);
  })();
</script>


<!-- Step 1. Option 3 - to add into website bundle by website developers -->
<script type="text/javascript" async defer src="//website.bundle.min.js"></script>

<!-- Step 2. For options 1 and 2 - use async init structure to start works with library  -->
<script>
  // (un)comment for debugging (enable)disable
  window.CFC_DEBUG = true;

  (function (f) {
    var g = window;
    if (g.cfc) f(); else (g.cfcAsyncInit ? g.cfcAsyncInit : g.cfcAsyncInit = []).push(f);
  })(function () {
    // widget is ready to init
    onWidgetReady(windows.cfc);
</script>

<!-- Step 2. For options 3 - use es6 modules for example to load chat library  -->
<script>
  import cfc from 'chat.widget';

  // (un)comment for debugging (enable)disable
  cfc.CFC_DEBUG(true);

  // widget is ready to init
  onWidgetReady(cfc);
</script>

<!-- Step 3. Work with library -->
<script>
  function onWidgetReady(cfc) {
   cfc.init({
      autoLoadLibraries:true // optional (default:true)
      // in case if website owner has own way to load firebase library
    });

    // login user to widget
   cfc.user
      .login({
        brandId: 44,
        userId: 67146891
    })
    .then(function () {
      // widget if logged-in there
    });
  }
</script>

Let’s see how library should be in high level design:

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global['cfc'] = factory());
}(this, (function () { 'use strict';

var isDebugVar = false;
function isDebug() {
  return (typeof CFC_DEBUG !== "undefined" && CFC_DEBUG) || isDebugVar;
}


function log() {
    if (isDebug()) {
      var args = Array.prototype.slice.call(arguments);
      args.unshift(window.console);
      Function.prototype.bind.apply(window.console.log, args)();
    }
}

// ... 

var ChatManager = function () {
  // ....
}();


var JsNS = 'cfc';

if (window[JsNS] === undefined) {

    // expose chat instance (singleton by design)
    var manager = ChatManager.inst?ChatManager.inst:(ChatManager.inst=new ChatManager());

    var widget = {
        user: {
            login: manager.login.bind(manager),
            // ... others fields
        },
        init: manager.init.bind(manager),
        manager: function() { return isDebug()?manager:null; };
        CFC_DEBUG: function(v) { isDebugVar = v; }
        // ... others fields
        VERSION: "0.0.1"
    };

    // run all async functions
    setTimeout(function() {
      var asyncInit = window[JsNS + 'AsyncInit'];
      if (asyncInit && asyncInit.length > 0) {
          for (var i = 0; i < asyncInit.length; i++) {
              try {
                  asyncInit[i]();
              } catch (e) {
                  error(e);
              }
          }
          asyncInit.length = 0;
      }
    });

    return widget;
} else {
    log('seems window.' + JsNS + ' already exposed');
}

})));

 

Conclusion

When all the rules from library design are followed, the result is a JS library that is not only fast and light, but can also be integrated in many different ways.

Pros

  • Compatible with all browsers
  • Has minimal dependencies
  • Doesn’t override native environment
  • Minimal size
  • Many ways to integrate
  • Ability to debug productions build

Cons

  • Very few fields are exposed to global
  • Dependent on a Firebase library

 

References