Tommy Ku's Method Stub

Reading and thinking

Have fun building browser start page

Posted on 2017.01.10
My current setup

Maybe it was my colleague, or maybe I feel the heat from /r/startpages… Whichever the reason is, I began building browser start page.

Default browser start pages are totally fine. Engineers’ put in hard work to make it convenient and visually pleasing, but it is not my page. Just as people customize their desktop wallpaper, I want my browser start page to do this and that while showing a refreshing background image.

More than just simple background image replacement, or listing bookmarks in various ways, it should show time and weather which are cool to look at but essentially useless.

The finished code is available at:

Setting things up

Using webpack as module bundler

In this start-page project, bower is substituted by webpack, a module bundler which compiles the source code along with modules used. In bower’s way, components are first put inside bower_components folder and then included in the HTML page one by one, while with webpack modules are loaded as simple as calling require('module-name').

# example.coffee
$ = require 'cash-dom'
$(document.body).text 'Hello World' # 'Hello World' is injected into <body>

Webpack uses loaders to pipe files through a set of processors when we call require such that gulp-sass and gulp-coffee are no longer needed.

# example.coffee
require '../css/app.scss'
# content of `app.scss` is passed through style-loader, css-loader, sass-loader and postcss-loader
# then injected into the page with JavaScript
# note: better keep a copy of the stylesheet inside <noscript> just in case
// webpack.config.js
module.exports = {
// ...
  module: {
    loaders: [
      { test: /\.scss$/, loader: "style-loader!css-loader?importLoaders=1!sass-loader!postcss-loader" },
    ]
  }
// ...
}

Using yarn along with npm

npm is fine but there exists yarn that runs faster and output better information while running npm|yarn install.

yarn generates a yarn.lock file that records packages with version numbers so the same packages are installed across machines.

For more of yarn vs npm see this post.

Brainstorm what to add

Browser start page is personal. There is no pattern or best practices associated with the genre.

Here is a list of stuff I might include:

# included in demo repo

The Startpage Emporium and /r/startpages are good places for seeking inspiration.

As often visited as a start page, it’s better kept small and fast. Consider importing external assets only when necessary and minimize the .js files in production build.

CSS grid

CSS grid is probably the reason for broken layout when you first build and open build from the demo repo.

How broken it looks with CSS grid disabled

The design shown of the demo is so simple it can be effortlessly implemented with grids from foundation, pure.css or bootstrap.

Flexbox is too a viable option if I want to avoid UI framework entirely, yet considering how troublesome flexbox is when implementing a 2D nested layout (mainly for the bloated meaningless HTML structure), I opted for CSS grid instead.

Fact: CSS grid isn’t widely supported (as of Jan 2017)

In Google Chrome 29 through 56, CSS grid is enabled through ‘experimental Web Platform features’ flags in chrome://flags, so I did that.

A grid is exactly a grid, a 2D layout structure with grid cells and lines defined using slightly abstract CSS syntax of grid-template-columns and grid-template-rows.

/* src/css/app.scss */
.grid-container {
  height: 100vh;
  display: grid;
  grid-template-columns: 12% 38% 38% 12%;
  grid-template-rows: 25% 25% 25% 25%
}

The above piece of CSS code defines a 4x4 grids with respective to cell sizes (or separation between grid lines). Other than an extra grid container that contains all grid elements, there is no need to change the existing HTML DOM structure to accommodate for the CSS grid layout.

I look forward to CSS grid being a default feature in major browsers. Meanwhile, a comprehensive guide is available on CSS tricks.

Chrome new-tab page override

Chrome pages namely Bookmark manager, History and New Tab pages can be replaced by arbitrary Chrome extension with chrome_url_overrides declared in manifest.json

Note other extensions might override your url overrides. In such case, disable those extensions.

/* manifest.json */
{
  "name": "Start page demo",
  "description": "New tab replacer demo",
  "version": "0.1",
  "incognito": "split",
  "chrome_url_overrides": {
    "newtab": "index.html"
  },
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
  "manifest_version": 2
}

Unfinished extensions are loaded into Chrome by clicking Load unpacked extension... in chrome://extensions page.

Sinitra web proxy on Docker

Have you experienced Same-origin policy getting in the way when you attempt to make a cross-origin HTTP request, which most likely to happen when making an API request to 3rd party service.

While it enhances security, it’s hindering legitimate API calls we want to make, specifically Dark Sky API for weather report.

# src/js/weather.js.coffee
request = require 'superagent'

module.exports = {
  getWeather: ()->
    request.get('http://0.0.0.0:1080/weather')
  # ...

In the demo repo weather.js makes a call to a local server listening at 1080 port which essentially wraps the Dark Sky API.

Make sure your’ve set the API key at the line ForecastIO.api_key = 'YOUR_DARK_SKY_API_KEY'.

The proxy server code is in web-proxy folder of the demo. With Docker installed, starte the server by running:

$ ./bin/build
$ ./bin/run

For those not running Docker, the server can still be started by:

$ bundle install
$ ruby server.rb

While the server itself is straight-forward proxy that returns exactly what Dark Sky API returns, Docker conveniently comes with --restart=always flag which starts the container automatically on reboot, allowing us to run the proxy with startpage with zero operational overhead.

Masking/faking it til it’s loaded

Masking the render

The page looks ugly for a split second before any CSS is injected and image loaded, then everything looks normal again. There should be an element over everything else masking all the ugly elements before the page is fully loaded.

<!-- output/index.html -->
<html>
  <head>
    <!-- ... -->
    <style>
      .mask {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: white;
        z-index: 10000;
        opacity: 1;
        transition: opacity 0.2s linear;
        will-change: opacity;
      }
    </style>
    <!-- ... -->
  </head>
  <body>
    <div class="mask"></div>
    <!-- ... -->
  </body>
</html>

The mask does only one thing: mask the page in white before (hopefully) some JavaScript set it’s opacity to 0, revealing the fully loaded page under it.

(exactly why you cannot click anything in the page…the mask element is still covering everything after unmasking)

edit: elements underneath the mask can be clicked by setting pointer-events: none on the mask

# main.coffee
# ...
WeatherPanel = require './weather_panel.js.coffee'
DateTimePanel = require './date_time_panel.js.coffee'
Wallpaper = require './wallpaper.js.coffee'

unMask = ->
  $('.mask').css {
    opacity: 0,
    'pointer-events': 'none'
  }

  $ ->
    (new Wallpaper()).bootstrap()
    (new WeatherPanel()).bootstrap()
    (new DateTimePanel()).bootstrap()
    unMask()

The demo uses a bad approximation on the timing of unmasking. Bootstraping the panels do not mean their respective assets (e.g. wallpaper, weather data) are fully loaded. This could be improved by a Promise-based approach on each bootstrapped class, but we are simplifying here.

Faking API call

As it turns out, everything looks fine even on unmasking (disclaimer: works on my machine), even the weather data is there despite our local proxy hasn’t returned anything yet.

# src/js/weather_panel.js.coffee
# ...
class WeatherPanel
  # ...
  updatePage: ->
    return unless @report?
    $('.temperature').html "#{@lo @report.temperature}&deg;"
    $('.summary').html @report.current_summary
    $('.weather-icon').prop src: "static/weatherIcons/#{@report.icon}.png"

  getWeatherHandler: (err, res)->
    return unless res? && res.body.currently?
    current = res.body.currently
    @report =
      icon: weather.getWeatherIcon(current.icon)
      current_summary: current.summary
      temperature: current.temperature
      hour_summary: res.body.hourly.summary
    store.set 'weather.report', @report # store @report object into localStorage
    @updatePage()

  bootstrap: ->
    weather.getWeather().end (err, res)=>
      @getWeatherHandler(err, res)
    @report = store.get 'weather.report' # retrieve @report object from localStorage
    @updatePage()

module.exports = WeatherPanel

The trick is to cache API call result, either by simply storing it into localStorage or add a service worker. In our case we are storing the data into localStorage, then update the page with the latest data from API when it becomes available.

Conclusion

This post illustrates the techniques I used when building my own startpage demo.

Although many corners were cut to ensure timely delivery such as

  1. only supports Google Chrome with flag enabling CSS Grid toggled
  2. code not really organized by components
  3. unmask on page loaded instead of on content loaded
  4. wallpaper not optimized per screen size
  5. used localStorage instead of service worker to cache API result

, many of the mentioned practices can be carried into general web developing and optimization.

And it’s always pleasant to see your own startpage isn’t?