Thursday 5 May 2016

Using JavaScript To Super Power Your Client's Shopify Site



JavaScript is in the midst of a renaissance that would make even LL Cool J jealous.



New frameworks, designed for pretty much everything imaginable, are popping up left and right at a breakneck pace. The proliferation of node and JavaScript on the server has encouraged an increased amount of dialog, spawning ideas like isomorpic JavaScript. Even traditional desktop applications like Spotify are being built (in part) using JavaScript.

In the front end, JavaScript has enabled us to do a number of interactive things like geolocate users and create rich animations.  However, I think the largest contribution made by JavaScript is something that’s largely gone unnoticed: speed.

JavaScript has been admirably trying to help improve speed on the Internet for some time now, however, it should probably go without saying that one of the biggest and most impactful changes on performance brought on by JavaScript was the arrival of the XMLHTTPRequest. XMLHTTPRequest is the underlying technology responsible for all of the ajax requests we make, allowing us to retrieve additional page resources without forcing a full page reload.  Oh, and it’s based on work done by everyone's favorite, Microsoft.

What about right now?

Let’s stop looking backwards and start talking about what JavaScript can do for us today. We’re going to look at some ways that we can leverage the Shopify platform in order to begin using JavaScript in more involved way on your clients' Shopify stores. I’ll show you some of the building blocks that you can put in place and then from there, the rest is up to you.

We'll start by defining what we’re trying to accomplish and what our goal really is. As you will shortly find out (or maybe even already know), once you start wandering beyond the safe and comfortable pastures of HTML and CSS, there’s a chance that you may never come back. All sorts of doors will start opening up for you, but for the sake of staying on track, let’s outline exactly what we’re gonna do today:

    Figure out how we’re gonna get all our product and collection data off of our Shopify server

    Set up templates in such a way to give us exactly the data that we want

    Write some JavaScript that enables us to grab all the data we need on initial pageload

    Address and solve any issues that present themselves along the way

Together, all of these improvements are going to create a smoother and snappier experience for users — one that tends to feel like a native app. Users will get to the things they want to see quicker, spend less time waiting for loads, and be more prone to further exploration and discovery. I also feel pretty confident that the effects of these benefits on items like conversion and bounce rate are pretty self-evident, so let’s instead just jump into some code and get this party started...

Act I:

The very first thing we need to figure out is how, and from where, we’re going to get our data from. Some sort of publicly available API that returned store data would be a good place to start, but unfortunately nothing like that exists...yet (more on that in a sec). There’s the public cart API, which also has a random product endpoint, but it’s really only helpful for executing cart actions. Also, the product endpoint only returns JSON for one item at a time, so if your store contained 300 products, that’s a cool 300 requests to the API every time a new user visits your site. Not exactly the direction we want to head in.

I’ve seen it mentioned in a few places that sending a $.getJSON request to a number of different places (e.g. “products/blue-hat”, “collections/all”), actually returns JSON for whatever thing was requested. This is sorta what we want, but still not exactly. Let’s forge ahead.

Once you start wandering beyond the safe and comfortable pastures of HTML and CSS, there’s a chance that you may never come back.
At this point, you might be saying, “Hey buddy, slow your roll. Maybe we can use one of those .json things to get some of the stuff we need!” And you wouldn’t be wrong — we could do that. However, we would still run into the problem of needing to make one request for every product and collection in our store. Also, there’s relatively little documentation explaining how the whole “URL + .json” thing even works. How do we know that sending a request to the exact same location tomorrow will return the same kind of data we got today? Maybe tomorrow we won’t get anything back.

Since we don’t know who, or what, decides what data we get back from those locations, I’m gonna suggest that we forget about them and move on. But before we do that, take a quick look at the URL we were sending the $.getJSON requests to...



O-m-quadruple-g, it was relative! the response was coming from INSIDE our own server!!

When you think about it, it makes perfect sense. Shopify servers are already set up to be more like APIs and less like static file servers. When people navigate to “http://store.com/collections/cute-motorcycle-jackets,” they’re not really seeing the file at that location. Instead, the Shopify server takes a look at the URL for the requested resource, sees that it needs to plug the “cute-motorcycle-jackets” data into the “collections” template, runs off really quickly to go build that template, and finally comes back to deliver a fully formed .html file.

Now consider this, what if we took one of our theme’s templates, say collections.liquid for instance, and ripped out all of the markup so there was only liquid tags. Then, imagine you navigated to “http://store.com/collections/rad-velcro-shoes,” which now uses the new markup-less collections template. Assuming we got rid of the liquid tags and filters that produced DOM nodes, what you should see is the markup from your theme.liquid template wrapped around a bunch of incoherent text from your collections.liquid template. If you altered the URL just a bit to “http://store.com/collections/rad-light-up-shoes,” and then tried loading that page, you would see something very similar to the last page, except with all the meaningless text updated to reflect items from the “rad-light-up-shoes” collection, as opposed to “rad-velcro-shoes.”

That right there (along with few other key moments I’ve conveniently glossed over) is our big win. It should make you realize that, within reason, the Shopify servers have to do exactly what we demand/politely ask them to do. All that needs to be done now is the legwork required to ensure the whole process runs smoothly. Luckily for you guys, I got your back and already did it.

Act II:

What we’re gonna do next is build some templates that only contain liquid tags and are structured like JSON. We’ll then make an ajax request for these templates as soon as we first initially land on our site. We’ll get a response back from the server, which will be the rendered template, containing all the data we initially set it up to hold. At this point, it’ll just be a long text string and not JSON, so we’ll need to run it through JSON.parse(). We’ll grab what we need and combine it with what we already have, and then link up all the products and collections so we can build a collection quickly, but without duplicating data anywhere.

We’re going to use a few features of the platform to help us accomplish all of this, namely, {% layout ‘none’ %}, alternative templates and ‘?view=’, and Taking Control of your Catalog Page.

Create a new collections template and name it something like "pagination-endpoint." It should look something like this:

{% layout none %}
{
  "totalCollections": {{ collections.size }},
  "totalProducts": {{ collections.all.products_count }}
}
view rawgistfile1.js hosted with ❤ by GitHub
If we’re going to request all of our products and collections from our Shopify server, we first need to know the total number of each before we start. This is due to the fact that when retrieving and displaying information on any liquid template, the max number of items returned (usually products) is capped at 50. To account for that, what we’ll do instead is keep incrementally asking for 50 products, (totalProducts/50) number of times.

This is also where we use the {% layout ‘none’ %} tag and Taking Control of your Catalog Page. The layout none tag tells the server, “when you build this particular template, don’t put it inside theme.liquid/layout frame, just give me the guts.” It makes it easier for us because now we don’t have to parse and trim all the stuff we don’t want in our response from the server. Following the instructions in the article above ensures that there’s a collection at ‘/collections/all/’, it has all our products in it (or whatever ones we want), and that we’re the ones in charge of it (the collection). This is where you could set whether or not you wanted out-of-stock items to be included in the data we’ll use to base our store off of.

Next, create a new list-collections template and name it something like "collections-endpoint." It should look something like this:

{% layout none %}
{% paginate collections by 50 %}
{
  "collections": [{% for collection in collections %}
{
  {% if collection.image %}"image": "{{ collection.image }}",{% endif %}
  "body_html": "{{ collection.description | url_escape }}",
  "handle": "{{ collection.handle }}",
  "id": {{ collection.id }},
  "sort_order": "{{ collection.default_sort_by }}",
  "template_suffix": "{{ collection.template_suffix }}",
  "title": "{{ collection.title }}",
  "products_count": {{ collection.products_count }},
  "url": "{{ collection.url }}",
  "products": []
}{% unless forloop.last %},{% endunless %}{% endfor %}
  ]
}
{% endpaginate %}
view rawgistfile1.js hosted with ❤ by GitHub
A few notes…

Because this is still a Liquid template that’s rendered on the server before it’s delivered to us, we can take advantage of conditional and control flow tags

{{ collection.description }} has a url_escape filter attached to it to preserve the HTML included in a product and collection’s description, while still making sure the description was properly formatted string for the JSON. My solution here is to url_encode the description on the server’s end, store the encoded string and transport it, and upon delivery, run it through decodeURI() to restore it.

Because we’re using an alternate template, we can still use the default collections.liquid and list-collections.liquid to serve content to crawlers trying to index the site. Alternate templates + pushState() should answer any question anyone has about SEO.

It might be retroactively prudent to look into the “json” filter

The products property is an empty array that we’ll populate when we’ve received all our collections and products back from the server, eternally linking our products and collections together forever.
Finally, create a new collections template and name it something like "products-endpoint." It should look something like this:

{% layout none %}
{% paginate collection.products by 50 %}
{
  "products": [{% for product in collection.products %}
{
  "available": {{ product.available }},
  "body_html": "{{ product.description | url_escape }}",
  "collections": [{% for collection in product.collections %}"{{ collection.handle }}"{% unless forloop.last %},{% endunless %}{% endfor %}],
  "handle": "{{ product.handle }}",
  "id": {{ product.id }},
  "images": [{% for image in product.images %}
        {
          "id": {{ image.id }},
          "position": {{ image.position }},
          "product_id": {{ image.product_id }},
          "src": "{{ image.src | img_url: 'large' }}"
    }{% unless forloop.last %},{% endunless %}{% endfor %}
  ],
  "options": [{% for option in product.options %}
        {
          "name": "{{ option }}",
          "position": {{ forloop.index }},
          "product_id": {{ product.id }}
    }{% unless forloop.last %},{% endunless %}{% endfor %}
  ],
  "product_type": "{{ product.type }}",
  "price": {{ product.price }},
  "price_max": {{ product.price_max }},
  "price_min": {{ product.price_min }},
  "tags": [{% for tag in product.tags %}"{{ tag }}"{% unless forloop.last %},{% endunless %}{% endfor %}],
  "title": "{{ product.title }}",
  "url": "{{ product.url }}",
  "vendor": "{{ product.vendor }}",
  "variants": [{% for variant in product.variants %}
        {
          {% if variant.image %}"image_id": {{ variant.image.id }},{% endif %}
          "available": {{ variant.available }},        
          "id": {{ variant.id }},
          "inventory_management": "{{ variant.inventory_management }}",
          "inventory_policy": "{{ variant.inventory_policy }}",
          "inventory_quantity": {{ variant.inventory_quantity }},
          "option1": "{{ variant.option1 }}",
          "option2": "{{ variant.option2 }}",
          "option3": "{{ variant.option3 }}",
          "position": {{ forloop.index }},
          "price": "{{ variant.price }}",
          "requires_shipping": {{ variant.requires_shipping }},
          "sku": "{{ variant.sku }}",
          "taxable": {{ variant.taxable }},
          "title": "{{ variant.title }}",
          "weight": {{ variant.weight_in_unit }},
          "weight_unit": "{{ variant.weight_unit }}"
    }{% unless forloop.last %},{% endunless %}{% endfor %}
  ]
}{% unless forloop.last %},{% endunless %}{% endfor %}
  ]
}
{% endpaginate %}
view rawgistfile1.js hosted with ❤ by GitHub
Keep in mind that you can pick and choose what goes into those templates (save for a few key properties). The only requirement is that what you get back from the server, HAS to be parsable JSON. If any of the data causes JSON.parse() to error, you’re gonna have a bad time.

(function(Resources, $, undefined) {
  // Private
  var requestLimit = 50;
  var collections = [];
  var collectionsHandleMap = [];
  var products = [];
  var productsHandleMap = [];

  var getResources = function() {

var $getCollections = function(totalCollections) {
  var collectionDeferreds = [];
  for (var i = 0; i < totalCollections/requestLimit; i++) {
    var pageNumber = 1 + i;
    var $collectionRequest = $.get('/collections?view=collections-endpoint&page='+pageNumber, function(response) {
      response = JSON.parse(response);
          $.merge(collections, response.collections);
    });
        collectionDeferreds.push($collectionRequest);
  }
  return $.when.apply($, collectionDeferreds).done(function() {
        collectionsHandleMap = $.map(collections, function(collection, index) {
          return collection.handle;
    });
  });
};

var $getProducts = function(totalProducts) {
  var productDeferreds = [];
  for (var i = 0; i < totalProducts/requestLimit; i++) {
    var pageNumber = 1 + i;
    var $productRequest = $.get('/collections/all?view=products-endpoint&page='+pageNumber, function(response) {
      response = JSON.parse(response);
          $.merge(products, response.products);
    });
    productDeferreds.push($productRequest);
  }
  return $.when.apply($, productDeferreds).done(function() {
        productsHandleMap = $.map(products, function(product, index) {
          return product.handle;
    });
  });
};

var $getActive = $.get('/collections/all?view=pagination-endpoint', function(response) {
  response = JSON.parse(response);
      $.when($getCollections(response.totalCollections), $getProducts(response.totalProducts)).done(function() {
        $.each(products, function(index, product) {
          $.each(product.collections, function(index, collectionHandle){
            collections[collectionsHandleMap.indexOf(collectionHandle)].products.push(product.handle)          
          });
        });    
        console.log(collections);
        console.log(products);
  })
    });

  };

  Resources.retrieveProduct = function(handle) {
return products[productsHandleMap.indexOf(handle)];
  };

  Resources.buildCollection = function(collectionHandle) {
var builtCollection = [];
var collection = collections[collectionsHandleMap.indexOf(collectionHandle)]
$.each(collection.products, function(index, handle) {
      builtCollection.push(products[productsHandleMap.indexOf(handle)]);
});
    return builtCollection;
  };


  Resources.init = function() {
    getResources();
  };

}(window.Resources = window.Resources || {}, jQuery))
view rawgistfile1.js hosted with ❤ by GitHub
Based on the above JavaScript, when getResources() is run, a series of requests are sent out to retrieve the information from the server, and once all the subsequent responses have been received and handled, lookup maps are created in order to retrieve products quickly without constantly and repeatedly needing to iterate over the array holding our data.

At this point, you have all the data and lookup tables you need in order to start building out an app.

In practice

For a live reference of what I was able to do with the above methodology, see SunStaches.com (and here for an experimental version using React).

Some high level stats: it takes about 1.5-2 seconds to request, receive and handle all of the data for a store with 337 collections, and 802 products with an average Internet connection. After that initial load, there are no more requests for products or collections required between the client and server. In all, the weight of all the requests/responses for the 300 odd collections and 800 odd items is about 250KB, which may seem large, but remember those are all asynchronous requests which can be retrieved concurrently. The largest single response weighs in at 17KB, with the average falling around 12KB.

After retrieving the initial payload of data, the only other requests that are ever made are for resources — like images — which are made once, on a JIT as needed basis, and then are cached and never retrieved again. If your site is lightweight enough you could preload/pre-cache all your images and cut out the need to make any more requests entirely. Just remember to be mindful of mobile users and users with slow internet connections before going too crazy with this.

After porting over SunStaches.com from WordPress (WooCommerce implementation) to Shopify (and using some of the methodologies described herein) the site has consistently ranked in the 90th-95th percentile of pagespeed scoring. Orders have been on the upswing and visitors have been enjoying a sleek, super-fast shopping experience.

Learn more

If you’re interested in exploring more, my advice would be to look into the following four ways to produce a full blown front-end application that’s still managed through the Shopify platform:

    React

    pushState

    Promises

    Module Pattern

Facebook  Twitter  Linkedin

About the author
Tyler Shambora is a front-end developer with BVAccel. Shambora hails from Palo Alto where he formerly worked with the team behind the Wildfire Pages App (eventually acquired by Google). Today he enjoys the San Diego sunshine and finding new ways to do cool things with Shopify.

No comments:

Post a Comment