Frontend Scrapbook

Notes that make a difference

Bundlers & Webpack 4

By admin

on Sun Sep 27 2020

There are actually 2 ways for javascript to work in browsers.

One way is it load using script tag or using inline script tag. The problem with this is that it doesn’t scale and too many scripts through script tag or html.

Browsers have a max number of default simultaneous persistent connections per server. Like Firefox 3+ has has 6, Safari 5+ has 6 and IE 10 has 8, Chrome has 6 etc

Even if you use HTTP2 doesn’t help as most of scalable applications have many modules usually ( like 3000 modules! ). It becomes unmaintainable.

Scope, Size, Readability, Fragility, Monolith Files ( a global.js file per html page for example ) were all problems in the realm of javascript.

Solution was to use IIFE or Immediately Invoked Function Expressions. It allows to provide data from an outside scope and return a scoped information.

var outerscope = 1;
const iife = (function(dataNowUsedInside) {
 var outerscope = 4; 
  return { someattr: 'data'; }
})(outerscope);
console.log(outerscope)  // outerscope = 1

If we didn’t have IIFE, we cannot have a variable of same name in the inners-cope as in the outer-scope. IIFE solves problems like encapsulation!

This lead to explosion of tools like Make, Grunt, Gulp, StealJS, Brunch etc. All these tools concatenate javascript files together, trying to solve problems. However, it still had problems like dead code, and full rebuilds every time!. Lots of IIFE’s are slow because you end up loading the whole library, lodash for example. We were missing lazy loading, hot module reloading, tree shaking etc.

Birth of Javascript Modules – Thanks to nodejs

CommonJS module format was introduced for nodejs to include javascript files in the backend as we don’t have access to DOM or script tags in the nodejs.

They use a syntax called require(‘modulename’) to import other modules from other files. We have named exports as well default exports.

//named exports
exports.add = (first, second) => first + second;
//default exports
module.exports = (first, second) => first + second;

const path = require('path'); // access default export
const {add} = require('./math') // access named export

It gives some static analysis. This is when explosion of javascript happened. Introduction of NPM, Node, Modules etc. The problem was that commonjs doesn’t have browser support. There is no live bindings ( problems with circular references ) and synchronous module resolution, loaders all were slow in commonjs. This resulted in popularity of Bundlers/Linkers.

Browserify ( static ), RequireJS ( Loader ), SystemJS ( Loaders ) all took your commonjs modules writen for web in commonjs and bundled to make code work in browser envs. We still had problems with this.

There were other patterns than commonjs, like AMD. It was too dynamic ( which slows down things at runtime )

define('myLib', ['lodash', 'somedep'], functon(_, someDep) {
 return { ... }
}
});

Then there was AMD + commonJS. This was non-standard and nor browsers or node supports it.

define(function(require, exports, module) {
 var _ = require('lodash');
 //..do things
 module.exports = someLib;
});

Then after around 10 years of work, there came ESM

import {uniq, forOf, bar} fom 'loadsh-es'
import * as utils from 'utils';

...
...
export const uniqConst = uniq([1,2,3,4]);

ESM are incredibly slow to a point that they are broken in the browsers. Even after just around 10 modules, it is slow!

Idea of webpack is that library authors use the module format that they like and choose.

Webpack is a module bundler let’s you write any module format, even mixed, compiles them for the browser. Supports static async bundling ( code splitting ) where you can create separate bundles at build time ( nothing is dynamic! ). It has rich, vast ecosystem.

Webpack config is just another commonjs module. It works even without a configuration. We could use a CLI if we don’t need a configuration.

module.exports = {
 entry : {
  vendor: './src/vendors.ts',
  main: './src/main.browser.ts'
 },
 output: {
  path: 'dist/',
  filename: '[name].bundle.js'
 }
}

The core concepts

Entry property

The first javascript file to load to ‘kick-off’ your app. webpack uses this as a starting point. This property complements the output propertry.

module.exports = {
 entry: './browser.main.ts'
}

Webpack creates a dependency graph from the entry point.

module.exports = {
 entry: './browser.main.ts',
 output: {
  path: './dist',
  filename: './bundle.js'
 }
}

Loaders and Rules

Tells webpack how to modify the files before it is added to dependency graph per file basis. Loaders are also modules ( functions ) that takes the source file, and returns it in a (modified) state.

module: {
 rules: [
  { test: /\.ts$/, use: 'ts-loader' },
  { test: /\.js$/, use: 'babel-loader' },
  { test: /\.css$/,use: 'css-loader' }
 ]
}
rules: [
 {
   test: /\.less$/,
   use: ['style', 'css', 'less']
 }

Less loader takes the .less file and creates a .css file. CSS loader takes .css file converts it into styles rules in memory as an array ( *.js file ) and style-loader consumes it and converts it into a javascript module which says take it and insert it as an inline style in browser.

Plugins

Objects with an ‘apply’ property on prototype chain. It allows you to hook into the entire compilation lifecycle. Webpack has a variety of built-in plugins. Plugins can access the whole bundle, where as loaders work per-file basis.

var webpack = require('webpack');

module.exports ={
 ...
 ...
 plugins: [
  new webpack.optimize.CommonChunkPlugin('vendors'),
  new webpack.optimize.UglifyJsPlugin()
 ]
 //...
}

CSS Loaders

module: {
   rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
   ],
},

Hot module replacement

Webpack has the ability to incrementally patch changes without having to reload the page.

"dev": "npm run webpack-dev-server -- --env.mode development --hot",

Extracting CSS to separate file, we will use a plugin called ‘mini-css-extract-plugin’

const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = () => ({
  output: {
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCSSExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [new MiniCSSExtractPlugin()],
});

file-loader and url-loader

Below configures loads jp(e)g images as base64 data uri for upto image sizes of 5000 bytes. Image sizes of more than this limit, it internally uses fil-loader and outputs image as a file to the dist directory and referred from within the javascript module.

  module: {
        rules: [
          {
            test: /\.jpe?g/,
            use: [
              {
                loader: "url-loader",
                options: {
                  limit: 5000,
                },
              },
            ],
          },
        ],
 },