Graduate Program KB

Webpack

Problems with script loading

  • Two ways to load a script in browser
    1. script tag with src
    2. script tag with js
  • Problems
    1. Doesn't scale
      • Too many scripts
      • Browser can only make a certain number of concurrent requests
    2. Unmaintainable scripts
      • If you use a single file
  • Solution
    • IIFE
      • Immediately invoked function expression
        const whatever = (function(...)){return ...})()
        
      • Encapsulate modules in their own scope
    • Now we can concatenate modules without concern that we will overwrite another module's scope
      • Need to rebuild
      • Have dead code
      • Lots of IIFEs are slow
        • Overhead in parsing and executing
    • Common JS
      • No browser support
      • No live binding, circular references
      • Slow module loading - synchronous
      • Can't statically analyses bundles
    • People wanted to start shipping web modules through npm
      • bundlers / linkers
        • Not everyone used CommonJS, other module formats like AMD are even more dynamic
    • ESM
      • Slow in the browser
    • Webpack
      • People ship any module format
      • Need something that
        • Can read every module format
        • Can handle resources and assets
      • Lets you write any module format and compiles for browser
      • Static async bundling
      • Lazy loading
      • Most performant way to ship js
    • Configuring webpack
      • webpack.config.js
        • Common JS module
      • CLI arguments
      • Node API

Webpack from scratch

  • npm scripts
    • node_modules/bin
    • scripts key in package.json
      "scripts" : {
          "webpack": "webpack"
          "dev": "npm run webpack -- --mode development"
          "prod": "npm run webpack -- --mode production"
      }
      
    • -- tells npm to add flags to the command
    • default
      • mode is production
      • entry is src/index.js
      • output is ./dist
  • Debugging
    • "debugthis": "node --inspect-brk ./src/index.js
    • "debug": "node --inspect-brk ./node_modules/webpack/bin/webpack.js
    • open chrome://inspect
    • Can connect to a remote target
    • Button for opening dev tools for node
  • First module
    • nav.js
      • export default "nav" for a single item
    • index.js
      • import nav from "./nav"
  • Watch mode
    • "dev": "npm run webpack -- --mode development --watch"
    • Will rebuild module
  • ES Module syntax
    • multiple exports

      export const top = "top";
      export const bottom = "bottom";
      
    • multiple exports, destructuring

      export {top, bottom};
      
    • multiple imports, with destructuring

      import {top, bottom} from "./footer.js"
      
    • Common JS syntax

      • Default export
        module.exports = (buttonName) => {return `Button: ${buttonName}`};
        
      • Named export
        const red = "color: red;"
        const blue = "color: blue;"
        exports.red = red;
        exports.blue = blue;
        
        • He recommends leaving exports at the bottom
      • Import, webpack interop use import with ESM instead of require
        import makeButton from "./button"
      
    • Webpack

      • Can't use CJS and ESM in the same file
      • Does support using require
  • Tree shaking
    • Webpack will remove code that can't be reached from the entry point
    • Not methods though
  • webpack.config.js
    • Common JS
    • Overrides defaults
  • Bundle walkthrough
    • Setting mode to none puts comments in the output
    • Module cache
    • require function
      • If module in cache return exports from cache
      • Create a new module and put in cache
      • Return exports
    • Execute entry module with scope, state, and require function
  • Import statements replaced with __webpack_require__

Core concepts

  1. Entry
  • Root of dependency graph
  • Simplest is a string path to file
  • Tells webpack what you want to include
  1. Output
  • Where and how we name the fie
output: {
  path: './dist',
  filename: './bindle.js'
}
  1. Loaders and rules
  • How to treat files that aren't js
  • If webpack finds a filename matching regex, use a loader to transform the file
  • Before added to dependency graph
  • Per file process
  rules: [
    {test: /\.ts$/, use: 'ts-loader'},
    ...
  ]
  • Chaining loaders
    • Run from right to left, like style(css(less()))
    rules:  [
      {
        test: /\.less$/
        use: ['style', 'css', 'less']
      }
    ]
    
  • Can do anything on a file, not just transform
    • e.g. output code coverage
  1. Plugins
  • Objects with an apply property
  • Hook into the entire compilation lifecycle
  • 80% of webpack is made up of it's own plugin system
function BellOnBundlerErrorPlugin() {}
BellOnBundlerErrorPlugin.prototype.apply = function(compiler) {
  if (typeof(process) !== undefined) {
    compiler.plugin('done', function(stats){
      if (stats.hasErrors()) {
        process.stderr.write('\x07');
      }
    });
    compiler.plugin('failed', function(err) {
        process.stderr.write('\x07');
    });
  }
}
module.exports = BellOnBundlerErrorPlugin;
var BellOnBundlerErrorPlugin = require('bell-on-error')
module.exports = {
  ...
  plugins: [
    new BellOnBundlerErrorPlugin()
  ]
}

Config

  • module.exports can also be a function that returns an object
  • "prod": "npm run webpack -- --env.mode production"
  • webpack takes the value of env and provides it to the config function
module.exports = ({mode}) => {
  return {
    mode,
    output: {
      filename "bundle.js"
    }
  }
}
  • html-webpack-plugin
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    ...
    plugins: [
      new HtmlWebpackPlugin,
      new webpack.ProgressPlugin()
    ]
    
    • Injects output into html index
    • Entry points are pre-page
    • Nee to create a plugin for each entry
  • Local dev server
    • webpack-dev-server
    • script: "webpack-dev-server": "webpack-dev-server"
    • web server based on express
      • Uses webpack-dev-middleware
      • Can use that directly with express for backend
    • generates bundle in memory and gives to express
  • Splitting configs
    • build-utils directory
    • npm install --dev webpack-merge
      • more or less just does Object.assign internally
      const webpackMerge = require('webpack-merge')
      const loadPresets = require("./build-utils/loadPresets")
      const modeConfig = env => require(`./build-utils/webpack.${env}`)(env);
      ...
      module.exports = ({mode, presets} = {mode: "production", presets: []}) =>
        webpackMerge( {
          {
            mode,
            plugins: [new HtmlWebpackPlugin(), new webpack.ProgressPlugin()]
          },
          modeConfig(mode),
          loadPresets({mode, presets})
        })
      
    • Use "[chunkhash].js" as prod output file?
      • Seems to make the browser realize it needs to refetch, since it's a different url?

Using plugins

  • CSS with webpack
    • import "./footer.css"
    • webpack.development.js
      {test: /\.css$/, use: ["style-loader", "css-loader"]}
      
  • Hot module replacement with CSS
    • --hot argument to webpack
    • Patch changes made incrementally and apply without reloading browser
    • webpack.production.js
      const MiniCssExtractPlugin = require('mini-css-extract-plugin)
      {test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"]}
      ...
      plugins: [new MiniCssExtractPlugin()]
      
      • Output to css file and add a style tag with url
        • Will put everything in one file, not scoped
      • Support for lazy loading CSS
  • File loader & URL loader
    {test: /\.jpe?g$/, use: "url-loader"}
    
    import imageUrl from "./webpack-logo.jpg" // base64 string
    const makeImage = (url) => {
      const image = document.createElement("img");
      image.src = url;
      return image;
    }
    const image = makeImage(imageUrl)
    
    • Limit file size
      • Bloating bundle with large file is not good for performance
      {test: /\.jpe?g$/, use: [{loader: "url-loader", options: {
        limit: 5000
      }}]
      
      • If below limit includes in bundle as base64, otherwise outputs it to dist and returns the url
  • Presets
    • Dev, prod, but also trying out features
    • loadPresets.js
    module.exports = env => {
      {presets} = env;
      const mergedPresets = [].concat(...[presets]);
      const mergedConfigs = mergedPresets.map(
        presetName => require(`./presets/webpack.${presetName}`)(env);
      )
      return webpackMerge({}, ...mergedConfigs);
    }
    
    • presets/webpack.typescript.js
      module.exports = () => ({
        module: {
          rules:[
            test: /\.ts$/, use: "ts-loader"
          ]
        }
      })
      
    • package.json
      • "prod:typescript": "npm run prod -- --env.presets typescript"
  • Bundle analyzer
    • npm install webpack-bundle-analyzer --save-dev
    • "prod:analyze": "npm run prod -- --env.presets analyze"
    • presets/webpack.analyze.js
      const WebpackBundleAnalyzer = require("webpack-bundle-analyzer").BundleAnalyzerPlugin
      module.exports = () => ({
        plugins: [new WebpackBundleAnalyzer()]
      });
      
    • Creates a dev server with a tree map visualization of the bundle
  • Compression
    • npm install compression-webpack-plugin --save-dev
  • Source maps
    • devtool, property for creating source maps
    • tradeoffs on how long builds take with source map quality
    devtool: 'source-map'