Archive

linkedin.com touch code review

linkedin.com has been in the blogs a lot about their pro-HTML5/JavaScript approach to build their apps. They were one of the first companies to build their back end (mobile backend) on node.js, and now they are following amazon cloud reader and financial times in pioneering HTML5 only mobile apps. Lately they have released iPad app which they claim is 95% in HTML5.

HTML5 for mobile devices is still a very young and undocumented technology, as due to the contrains of the mobile device hardware and network speed traditional optimization best practices don't really apply here.

So I took took at their mobile app on: http://touch.www.linkedin.com to learn something from their R&D team and here are the findings:

The code is structured with pretty advanced inheritance pattern. It does little remind me of sencha touch syntax. Classes are created via .extend(). Here is an example:

TLI.ListItemView = TLI.BaseView.extend({
  initialize: function () {
    throw "This class must be subclassed!";
  },
  activateUrl: function () {},
  activate: function () {
    var a = this.activateUrl();
    if (a) window.location.href = a
  },
  remove: function () {
    var a = this.$(".list_item_container"),
      b = $(this.el);
    b.height(a.height());
    a.fadeOut(function () {
      b.slideUp()
    })
  }
});

All CSS and javascript are inline on the page. (dependancies are injected later) No cashing offline mode or anything like that...

linkedin mobile developers are not using any third party mobile libraries. Yep, I could not find a good one either. reading about all the minimalism and performance advices sencha touch or jQuery mobile just don't do it for me either, plus it takes all the fun away from the R&D development. The only third party lib I see is Backbone.

Instead of images, I see HTML5 canvas being used as a loader spinner, and even to draw the logo. NICE!

One of the optimization best practices for mobile HTML5 web dev is minimize parsed JavaScript parsing. I do se at least one object to be "pre-parsed" stored as a string:

window._serverData = ' {
  "locale": "en-us",
  "buckets": {
    "rateApp": "invite-rate",
    "homeNewsNum": {
      "name": "one",
      "payload": {
        "count": 1,
        "viewkey": "m_abtest_1_optionD",
        "clickkey": "m_abtest_1_optionD_tap"
      }
    },
    "ads": {
      "name": "A",
      "payload": {
        "enabled": false
      }
    },
    "adsText": {
      "name": "Other",
      "payload": {
        "leadtext": "send_me_more_info",
        "responsetext": "more_info_response",
        "disabledtext": "request_sent"
      }
    }
  }
}';

HTML5 Local storage is used for stroring user session data (no cookies) and also is used for storring dependancies of the framework. NICE! Actually here is their local storage class:

TLI.Storage = {};

(function (g) {
  
  g.$localStorage = localStorage;
  g.simulateWriteFailure = false;
  g.nonVolatileKeys = {
    "TLI:applicationLog": true,
    first_run: true
  };
  
  g.ensureVersion = function () {
    var a = g.generateKey("storageVersion"),
      b = TLI.config.storageVersion;
    if (g.$localStorage[a] !== b.toString()) {
      g.clear();
      try {
        if (g.simulateWriteFailure) throw "Write failure";
        g.$localStorage[a] = b
      } catch (c) {
        TLI.localStorageAvailable = false;
        $log.error("Local storage write failure - " + c)
      }
    }
  };
  g.get = function (a, b) {
    if (!b) b = JSON.parse;
    var c = null;
    if (TLI.localStorageAvailable) {
      g.ensureVersion();
      if ((c = g.$localStorage[a]) && c.length) return b(c)
    }
    return c
  };
  g.set = function (a, b, c) {
    if (!c) c = JSON.stringify;
    if (TLI.localStorageAvailable) {
      g.ensureVersion();
      if (b === null || typeof b === "undefined") g.$localStorage.removeItem(a);
      else try {
        if (g.simulateWriteFailure) throw "Write failure";
        g.$localStorage[a] = c(b)
      } catch (d) {
        if (d.name === "QUOTA_EXCEEDED_ERR") {
          console.log("****QUOTA_EXCEEDED_ERR: Resetting local storage...****");
          g.reset();
          g.$localStorage[a] = c(b)
        } else {
          TLI.localStorageAvailable = false;
          $log.error("Local storage write failure - " + d)
        }
      }
      return b
    }
    return null
  };
  g.remove = function (a) {
    g.set(a, null)
  };
  g.generateKey = function (a, b) {
    var c = "TLI:";
    if (b) c += "user:" + b + ":";
    c += a;
    return c
  };
  g.clear = function () {
    TLI.localStorageAvailable && g.$localStorage.clear()
  };
  g.createIndex = function (a) {
    g.set(a + ":index:undefined", {})
  };
  g.search = function (a) {
    g.get(a + ":index:undefined");
    return []
  };
  g.reset = function () {
    g.nonVolatileKeys[$storage.generateKey("notif:inv:timestamp", TLI.User.getCurrentUser().id)] = true;
    g.nonVolatileKeys[$storage.generateKey("notif:msg:timestamp", TLI.User.getCurrentUser().id)] = true;
    var a = $storage.generateKey("inbox", TLI.User.getCurrentUser().id),
      b = $storage.generateKey("connections", TLI.User.getCurrentUser().id),
      c = $storage.generateKey("profile", TLI.User.getCurrentUser().id);
    g.nonVolatileKeys[a] = true;
    g.nonVolatileKeys[a + ":timestamp"] = true;
    g.nonVolatileKeys[b] = true;
    g.nonVolatileKeys[b + ":timestamp"] = true;
    g.nonVolatileKeys[c] = true;
    g.nonVolatileKeys[c + ":timestamp"] = true;
    var d = {};
    _.each(g.nonVolatileKeys, function (e, f) {
      if (f) d[f] = g.get(f)
    });
    g.clear();
    _.each(d, function (e, f) {
      g.set(f, e)
    })
  }
})(TLI.Storage);

Not all the code is loaded at once I assume, what is on the page is just the core framework. Here is their "loader" class that can load js/CSS, even cross domain I see...

TLI.Loader = {};
(function (g) {
  var a = document.getElementsByTagName("script")[0],
    b = document.getElementsByTagName("link")[0],
    c = "localStorage" in window,
    d = function (h) {
      return "TLI:assets:" + h.split("/").pop()
    },
    e = function (h) {
      setTimeout(function () {
        h && h.call(this)
      }, 0)
    };
  g.injectJS = function (h, j) {
    var l = document.createElement("script");
    l.type = "text/javascript";
    l.innerHTML = h;
    a.parentNode.insertBefore(l, a);
    e(function () {
      j && j.call(this)
    })
  };
  g.sourceExternalJS = function (h, j) {
    var l = document.createElement("script");
    l.src = h;
    l.type = "text/javascript";
    a.parentNode.insertBefore(l, a);
    l.onload = j
  };
  g.injectCSS = function (h, j) {
    var l = document.createElement("style");
    l.type = "text/css";
    l.innerHTML = h;
    b.parentNode.insertBefore(l, b);
    e(function () {
      j && j.call(this)
    })
  };
  g.sourceExternalCSS = function (h, j) {
    var l = document.createElement("link");
    l.rel = "stylesheet";
    l.href = h;
    b.parentNode.insertBefore(l, b);
    e(function () {
      j && j.call(this)
    })
  };
  g.injectAsset = function (h, j, l) {
    if (h === "js") g.injectJS(j, l);
    else h === "css" && g.injectCSS(j, l)
  };
  g.sourceExternalAsset = function (h, j, l) {
    if (h === "js") g.sourceExternalJS(j, l);
    else h === "css" && g.sourceExternalCSS(j, l)
  };
  g.fetchAsset = function (h, j) {
    var l = new XMLHttpRequest;
    l.open("GET", h.path, true);
    l.onreadystatechange = function () {
      (!l.readyState || l.readyState == "loaded" || l.readyState == "complete" || l.readyState == 4) && j.call(this, l.responseText)
    };
    l.send(null)
  };
  g.assetCallbacks = {};
  g.fetchCrossDomainAsset = function (h, j) {
    var l = document.createElement("script");
    l.type = "text/javascript";
    var o = h.path;
    if (h.path.match(/\.js$/)) o = h.path.replace(/\.js$/, ".jsonp.js");
    else if (h.path.match(/\.css$/)) o = h.path.replace(/\.css$/, ".css.jsonp.js");
    l.src = o;
    g.assetCallbacks[h.name] = j;
    a.parentNode.insertBefore(l, a)
  };
  var f = function (h, j, l) {
      if (TLI.config.enableUpdatePatching && TLI.config.bundleUpdates && typeof TLI.diff_match_patch !== "undefined") if (j = TLI.config.bundleUpdates[h.name + ":" + j + ":" + l]) return {
        name: h.name,
        path: j
      };
      return null
    },
    i = function (h, j) {
      var l = d(h.name),
        o = d(h.name) + ":version";
      g.fetchCrossDomainAsset(h, function (n, p) {
        try {
          g.ensureAssetStorageVersion();
          if (n === h.version) {
            localStorage[l] = p;
            localStorage[o] = h.version
          }
        } catch (q) {
          c = false
        }
        j(p)
      })
    },
    k = function (h, j, l) {
      var o = d(h.name),
        n = d(h.name) + ":version";
      g.fetchCrossDomainAsset(j, function (p, q) {
        var t = null;
        try {
          g.ensureAssetStorageVersion();
          t = localStorage[o];
          if (p === h.version) {
            q = JSON.parse(q);
            a: {
              for (var r = (new TLI.diff_match_patch).patch_apply(q, t), s = 0, u = r[1].length; s < u; s++) if (!r[1][s]) {
                t = null;
                break a
              }
              t = r[0]
            }
            if (t) {
              localStorage[o] = t;
              localStorage[n] = h.version
            } else {
              i(h, l);
              return
            }
          }
        } catch (v) {
          c = false
        }
        l(t)
      })
    },
    m = function (h, j, l) {
      var o = d(j.name),
        n = d(j.name) + ":version";
      TLI.Storage.ensureVersion();
      var p = null;
      n = localStorage[n];
      if (j.version.toString() === n) {
        p = localStorage[o];
        g.injectAsset(h, p, l)
      } else(o = f(j, n, j.version)) ? k(j, o, function (q) {
        g.injectAsset(h, q, l)
      }) : i(j, function (q) {
        g.injectAsset(h, q, l)
      })
    };
  g.load = function (h, j) {
    var l = h.path.split("."),
      o = l[l.length - 1];
    l = TLI.detect(navigator.userAgent);
    if (l.android && !l.versionAtLeast("2.2")) g.sourceExternalAsset(o, h.path, j);
    else TLI.config.cacheAssets && c ? m(o, h, j) : g.fetchAsset(h, function (n) {
      g.injectAsset(o, n, j)
    })
  };
  g.loadedAssets = {};
  g.loadAll = function (h, j) {
    var l = 0,
      o = function () {
        if (l >= h.length) j && j.call(this);
        else {
          var n = h[l];
          l++;
          if (g.loadedAssets[n.path]) o();
          else {
            g.loadedAssets[n.path] = true;
            g.load(n, o)
          }
        }
      };
    o()
  };
  g.requireBundles = function (h, j) {
    for (var l = [], o = 0, n = h.length; o < n; o++) {
      var p = h[o],
        q = TLI.bundles[p];
      if (q) {
        q.name = p;
        l.push(q)
      }
    }
    g.loadAll(l, j)
  };
  g.requireBundle = function (h, j) {
    g.requireBundles([h], j)
  };
  g.recvAssetFile = function (h, j, l) {
    (h = g.assetCallbacks[h]) && typeof h === "function" && h.call(this, j, l)
  };
  g.clearAssetStorage = function () {
    for (var h in localStorage) h.match("^TLI:assets:") && localStorage.removeItem(h)
  };
  g.ensureAssetStorageVersion = function () {
    var h = TLI.config.assetStorageVersion,
      j = localStorage["TLI:assets:storageVersion"];
    if (!j || j.toString() !== h.toString()) {
      g.clearAssetStorage();
      localStorage["TLI:assets:storageVersion"] = h
    }
  }
})(TLI.Loader);

Templating is done via backbone and so is routing I suppose. Templates are placed in js files as a object literal.

Also see zeptojs included

No responsive design here! every device has a separate CSS file which is loaded based on JavaScript detection.

base64 image inlining is used heaviliy in the CSS

Comments: