A simple framework for developing 3rd party JavaScript widgets

Almost two years ago I quit a desk job and decided to work remotely and that was one of the best choices I’ve ever made! As a remote software developer I rely a lot on notifications - our team communicates on Flowdock and we also use Github, Trello, Invision and many other tools. I gotta constantly check for notifications from all of these tools to get my work done. Unfortunately, most of the software have their notification system broken! You either get too many notifications or don’t get the ones you want. You get them on desktop but not on the mobile or vice versa! Building a reliable and usable notification system is a complex task and therefore we decided to build a service called MagicBell to help you take on this challenge.

One of the components of our service is a 3rd party JavaScript widget that you can install in your web app. Let me show you in this blog post how you can create a simple framework to write JavaScript widgets and distribute some of your magic!

This is ours ;)

The MagicBell 3rd party Javascript Widget

Drink lots of water before proceeding and get that grey matter of yours ready to do some lifting…

Scoring Goals!

Before getting our hands dirty, let’s define some basic goals:

  • Our widget should be isolated from other code. It must not interfere with code on the host page, and host code should not interfere with your widget. This is the most fundamental and important goal of any widget.
  • The code your users have to download should have the smallest possible footprint. Remember “premature optimization is the root of all evil”, so don’t worry too much about this in the beginning. Just keep an eye out.
  • The widget will have some external dependencies (like jQuery/Zepto, React), but others are going to be bundled into the code you provide. Bundling some dependencies makes the installation of your widget hassle-free. But more importantly, this is going to impact directly the isolation level of your widget. Spend some time considering which dependencies you are going to include externally and find the balance between the first and the second point.

The Toolchain

Let’s get trendy and use the hottest stuff for front-end development nowadays:

  • webpack for bundling and distributing your widget.
  • ES6, we’re going to use Babel to compile the code.
  • Sass, I hear someone whispering “Less”.
  • React or any other cool framework.

And of course, do not skip tools for TDD:

Project Preparation

First, make sure you have Node and NPM installed. You can download it from https://nodejs.org/en/download or be awesome and open your terminal:

# For Ubuntu/Debian
apt-get install nodejs
apt-get install npm

# For macOS
brew install node

Cool. Now let’s create a folder and initialize NPM for our project:

mkdir js-widget
cd js-widget
npm init

webpack Configuration

Over the past years I worked with Grunt, Gulp, Browserify and the rails asset pipeline. This was my first experience with webpack. If you haven’t tried it yet you should really give it a try. I won’t explain webpack configuration in detail because its documentation is pretty good if you want more information.

Let’s install and add it to our project with npm i --save-dev webpack and then create a basic configuration file for webpack:

// ./webpack.config.js

const path = require('path');
const webpack = require('webpack');


module.exports = {
  entry: './src/index.js',                   // The widget's entry point
  output: {
    path: path.resolve(__dirname, 'dist'),   // An absolute path to the output directory
    filename: 'widget.js'                    // The name to use for the output file
  },
  plugins: [
    new webpack.NoEmitOnErrorsPlugin(),      // Do not emit the bundle if there are errors while compiling
    new webpack.HotModuleReplacementPlugin() // Enable HMR, of course
  ]
};

It is very useful to test your code as you develop, and webpack ships with a very flexible Express server. The webpack-dev-server is pretty good for prototyping so install it with npm i --save-dev webpack-dev-server and create an index.html file that imports your bundle:

<!-- ./index.html -->

<!DOCTYPE html>
<html>
<head>
    <title>My cool widget</title>
</head>
<body>
    <script type="text/javascript" src="/widget.js"></script>
</body>
</html>

OK! So how do we start the dev server now? Add the command to your NPM scripts and make your life easier. Open the package.json file and define the start script:

// ./package.json

{
  ...
  "scripts": {
    "start": "webpack-dev-server --open"
  },
  ...
}

Now you can just enter npm start in your terminal and start developing! All changes are going to be automatically compiled and loaded.

At this point, your project tree should look like this:

js-widget/
|-- node_modules/
|-- src/
|   |-- index.js
|-- index.html
|-- package.json
|-- webpack.config.js

ES6, ES2015, Harmony…

So confusing! For the sake of brevity let’s call it ES6. As mentioned above, we’re going to write ES6 code and compile it with Babel. This step is absolutely unnecessary if you prefer to just use plain old JavaScript.

webpack has to apply some transformations to our code (compiling it to JS) before packing it. These transformations are done with webpack “loaders”. Therefore, install the Babel compiler with npm i --save-dev babel-cli babel-preset-env and also the Babel loader npm i --save-dev babel-loader.

Now configure Babel for webpack:

// ./webpack.config.js


module.exports = {
  ...
  module: {
    rules: [{
      test: /\.js$/,                              // Compile all .js files with Babel
      exclude: /(node_modules|bower_components)/, // Do not transform npm  and bower packages, add other unwanted sources
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['env']                        // Run all transforms
        }
      }
    }]
  }
};

Oh yes, there are webpack loaders for TypeScript and CoffeeScript too if you prefer either of those.

You’ve got style!

And very often you would want your widget to also have some styling. To achieve it with webpack you gotta do something I find unintuitive: extract the CSS “text” into a separate file! But don’t panic, there’s a plugin for that, install it with npm i --save-dev extract-text-webpack-plugin.

Update: Nisanth Chunduru just pointed out that we can also use the style loader module to make your CSS be appended in a style tag to any DOM element you want. You can install it with npm i --save-dev style-loader.

How does webpack know how to handle our SCSS files though? Loaders, right! We gotta install and setup loaders for CSS and SCSS: npm i --save-dev css-loader sass-loader. Also install the compiler for Node.js npm i --save-dev node-sass.

Now configure webpack to handle stylesheet files and build our final stylesheet bundle:

// ./webpack.config.js

const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');


module.exports = {
  ...
  plugins: [
    ...
    new ExtractTextPlugin('widget.css') // Extract all stylesheets to a unique bundle
  ],
  module: {
    rules: [{
      ...
    },{
      test: /\.(scss|css)$/,            // Move all the required *.css and *.scss modules in entry chunks into a separate CSS file
      use: ExtractTextPlugin.extract({  // Create an extracting loader
        use: [{
          loader: "css-loader"          // webpack automatically will choose the proper loader
        },{
          loader: "sass-loader"
        }],
      })
    }]
  }
};

Now you can create an empty theme.scss file in the src folder and import the bundled file to preview while you’re developing:

<!-- ./index.html -->

...
<body>
    <link rel="stylesheet" href="/widget.css">
    <script type="text/javascript" src="/widget.js"></script>
</body>
...

IMPORTANT: Unfortunately the file is not going to be compiled unless you import it in the specified entry file:

// ./src/index.js

import style from './theme.scss';

I’m pretty sure you have a good idea on how to use Less instead of Sass, just install the less-loader and add it to the list of loaders in webpack.config.js.

Setup is boring, I know. Let’s go where the money is!

Our cool widget

Our cool widget is going to render a simple “Hello World” message with Preact (remember our second goal? Keep the download size to a minimum. Besides, Preact is so cool! Isn’t it?). Let’s install it npm i --save preact, and add JSX support npm i --save-dev babel-plugin-transform-react-jsx.

// ./src/views/hello_world.js

import { h, render, Component } from 'preact';


export default class HelloWorld extends Component {
  render() {
    return <span>Hello World!</span>;
  }
}

Let’s import and use this component in our entry file:

// ./src/index.js

import style from './theme.scss';

import HelloWorld from './views/hello_world';
import { h, render } from 'preact';


render(<HelloWorld/>, document.body);

So far, so good! However, the API of our widget is ugly. We can do it better:

// ./src/index.js

import style from './theme.scss';

import HelloWorld from './views/hello_world';
import { h, render } from 'preact';


export function initialize(options = {}) {
  render(<HelloWorld/>, options.target);
};

But nothing is rendered now! Open the browser console and you won’t find our initialize function either. We need to make our widget code available when imported. Let’s get back to webpack configuration:

// ./webpack.config.js

module.exports = {
  ...
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'widget.js',
    library: 'JSWidget',   // Name of the library
    libraryTarget: 'umd'   // Make it available to all sorts of environments
  },
  ...
};

Now restart the app and in your browser console inspect(JSWidget). You can initialize your widget and pass options to it.

var options = { target: document.body };
var widget = JSWidget.initialize(options);

Distributing your widget

All of the previous work is useless if you can’t distribute your plugin! At this point your widget is more than 360kB! Compression helps but you also want to upload your code to a CDN so it can be served really fast to users across the world. (I like to think of it as the “Appian Highway” of Internet).

Start writing a script for preparing your code for staging and production environments:

// ./package.json

{
  ...
  "scripts": {
    "start": "webpack-dev-server --open",
    "build:production": "webpack -p --config webpack.production.config.js",
    "build:staging": "webpack -p --config webpack.staging.config.js",
  },
  ...
}

Cool! Now npm run build:staging will build your code for staging and npm run build:production for production.

You can duplicate your webpack.config.js file and start adding plugins to the build for that environment:

// ./webpack.production.config.js

...
const S3Plugin = require('webpack-s3-plugin'); // Install the plugin with npm i --save-dev webpack-s3-plugin


module.exports = {
  ...
  plugins: [
    ...
    new webpack.optimize.UglifyJsPlugin(), // Compress and mangle with UglifyJS
    new S3Plugin(                          // Upload your bundle to S3, omit this if don't use AWS
      include: /.*\.min\.(css|js)$/,       // Upload distribution files only
      s3Options: {                         // Use an AWS credentials file, the plugin can read keys from that file
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      },
      s3UploadOptions: {
        Bucket: ''                         // Set the name of your S3 bucket
      },
      cloudfrontInvalidateOptions: {       // Automatically invalidate your Cloudfront distribution cache every time you upload your code
        DistributionId:
        Items: ["/*"]
      }
    ),
    new webpack.BannerPlugin({             // Include a banner with important info like version, licence, etc
      banner: "JSWidget v1.0.0"
    }),
  ],
  ...
};

You can duplicate this file for the staging configuration.

Update: Your brain might be a bit dry by now. Drink another glass of water and give your webpack confguration some DRYness with webpack-merge. You can install it with npm i --save-dev webpack-merge, now you can create a webpack.base.config.js file and use it in your other envs configuration files easily:

// ./webpack.production.config.js
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.config.js');

module.exports = merge(baseConfig, {
  // Production-specific configuration
  plugins: [
    new webpack.optimize.UglifyJsPlugin()
  ]
});

Woohoo! You’ve built a basic framework for writing a 3rd party Javascript widget. There are other topics to discuss (like sandboxing, right? Rest easy, we’ll talk about this important topic in a next blog post) and this framework still has lots of room for improvement.

I hope you build something awesome. Show it to us or ask us anything by tweeting us at @magicbell_io! You can also follow us on Twitter to read more blog posts on building 3rd party Javascript widgets and the APIs needed to drive them.



blog comments powered by Disqus
Josue Montano
Josue Montano