Course Notes

Webpack 5

1 introduction

  • Create a basic javascript app
  • Create a webpack configuration for this application
  • Look at loaders and asset modules
  • Etc, just an outline of the course

2 what you need for this course

3 Why do we need webpack?

  • Back in the day, css and javascript was imported at the head of a html file. There could be a lot of files and the order in which they were imported was important, since one file might depend on another file.
  • Grunt and Gulp were used to manage assets and concatenate javascript files into one
    • These did not manage dependencies between those files
  • To manage javascript dependencies, we used Require.js
    • Helps, but not as powerful as webpack
  • Webpack
    • Is a static module bundler for modern JavaScript applications
    • Webpack recursively builds a dependency graph that includes every module in your application and packages all modules into one or more bundles.
    • Using webpack, we only import one javascript file and one css file into our html doc
    • No need to worry about dependencies or the import order
    • Everything is bundled in one file
  • Webpack is a single tool for managing all code and assets in one place - Javascript, typescript, coffeescript, css, SASS, LESS, images, fonts and more

4 Setting up our application

  • Index.html
1<body>
2  <script src="./src/index.js"></script>
3  <script src="./src/hello-world.js"></script>
4</body>
  • Note index is imported before hello-world
  • Hello world has a function definition: helloWorld which console logs
  • helloWorld is invoked in index.js
    • index.js depends on hello-world.js
  • running in live server, we get an error since helloWorld is not available to index.js
  • by changing the order of the script tags, our code works

5 installing webpack

  • webpack can generate multiple bundles
    • doesn’t have to be a single bundle
    • we’ll look at different strategies for generating bundles later on
  • install webpack and webpack-cli:
  • webpack-cli is used to run webpack from the terminal
    • npm i webpack webpack-cli --save-dev

6 note about github repo

There are 2 git branches per each lesson that implies code changes. Every git branch name consists of the lesson number and lesson name. For example, for Lesson 10 "Handling Images with Webpack" the names of the branches are 10-handling-images-with-webpack-begin and 10-handling-images-with-webpack-end. 10-handling-images-with-webpack-begin branch represents the state of the repository at the beginning of the Lesson 10, while 10-handling-images-with-webpack-end branch represents the state of the repository at the end of the Lesson 10. Git repo.

7 integrating webpack into our JavaScript app

  • remove the hello world file from index.html, we only need index.js
    • need to import helloWorld function into
1import helloWorld from "./hello-world.js";
  • we export from hello-world.js
1export default helloWorld;
  • this is the import/export syntax is used by ecmascript modules
    • webpack supports this syntax by default
  • now, run webpack: npx webpack, and we get this:
1C:\Users\winklevi\Desktop\Webpack 5 Course>npx webpack
2asset main.js 50 bytes [emitted] [minimized] (name: main)
3orphan modules 93 bytes [orphan] 1 module
4./src/index.js + 1 modules 187 bytes [built] [code generated]
5
6WARNING in configuration
7The 'mode' option has not been set, webpack will fallback to 'production' for this value.
8Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
9You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
10
11webpack 5.88.2 compiled with 1 warning in 372 ms
12
13C:\Users\winklevi\Desktop\Webpack 5 Course>
  • Note, we ran webpack with no configuration file
    • Normally you would provide a config file
    • If none provided, it uses its own default
  • The mode option
    • Makes it easy to set up different configs for production and development
  • Webpack has created a main.js file in dist folder that contains:
1(() => {
2  "use strict";
3  console.log("hello world");
4})();
  • How did webpack know where to get our javascript to create main?
  • We run webpack with "--stats detailed" option
    • The entrypoint is the file that webpack uses as a starting point when building your app
    • Since we don’t specify the entrypoint for our app, webpack assumes index.js in the src folder is where it is to begin

8 custom webpack config

  • webpack stores its config in a js file typically called webpack.config.js.

    • we create a webpack.config.js for our app
    • our minimal webpack.config.js file (similar to webpack defaults)
    1module.exports = {
    2  entry: "./src/index.js",
    3  output: {
    4    filename: "bundle.js",
    5    path: "./dist",
    6  },
    7  mode: "none",
    8};
  • we get an error because we are using a relative path, but we need an absolute path

    • need the nodejs path package to generate absolute path
      • must still import using require
    1const path = require("path");
    2...
    3 output: {
    4  filename: "bundle.js",
    5  path: path.resolve(__dirname, "./dist"),
    6},
  • we run webpack and it works
  • note, webpack creates a bundle.js file for use
    • we are still referencing index.js in our html
    • we update our src attribute for our script tag
  • to run webpack conveniently, we can create a script in package.json
1 "scripts": {
2    "test": "echo \"Error: no test specified\" && exit 1",
3    "build": "webpack"
4  },

9 Asset Modules

  • Asset modules, new to webpack 5.
    • easily use asset files in javascript app without installing extra dependencies
    • must configure in config file so webpack knows how to import asset files
    • assets: images, fonts, plain text files
  • 4 types of asset modules
    • asset/resource
      • emits file into output directory and exports url to the file
      • use for large images, large font files
    • asset/inline
      • inlines file into bundle as data URI
      • small asset files like SVGs
      • injected into bundle as data URI
      • doesn't generate a new file in output folder
    • asset
      • combination of inline and asset/resource
      • webpack automatically chooses whether to use inline or resource (<8kb, treated as inline)
        • 8kb value can be changed with config
    • asset/source
      • plaintext data
      • imported as is as a string of text

10 Handling images with webpack

  • we create a new file called add-image.js:
1import kiwi from "./kiwi.jpg";
2
3const addImage = () => {
4  const image = document.createElement("img");
5  img.alt = "Kiwi";
6  img.width = 300;
7  img.src = kiwi;
8  const body = document.querySelector("body");
9  body.appendChild(image);
10};
11
12export default addImage;
  • I tried to run webpack at this point and got an error obviously
    • we still have to update the config file so webpack can handle images
  • we add a property to our webpack.config:
1module: {
2  rules: [
3    {
4      test: /\.(png|jpg)$/,
5      type: "asset/resource",
6    },
7  ];
8}
  • test is a regular expression matching png or jpg
  • second property is either type or use
    • use is to do with loaders
    • type relates to asset modules
  • type can be:
    • asset/resource
    • asset/inline
    • asset/source
    • asset
  • remember, we use asset/resource for images
  • the image is output to the dist folder
    • it is renamed to the MD5 hash of the contents of the file by default
  • running npm run build and checking our web page, we see we have our image displayed

11 publicPath

  • publicPath is a config option
    • tells webpack which url to use to load generated files in the browser
    • example, using static files like images, can tell browser where those files can be taken from using publicPath
    • can worry about publicPath less in webpack 5 than previously
  • by default, webpack 5 sets publicPath to 'auto'

    • previously the default was an empty string
    • if webpack is run with publicPath set to "", then we just get the name of the file as the img src in our index.html instead of the full path and the image is not displayed
  • publicPath in the config using relative path:

    1 output: {
    2  filename: "bundle.js",
    3  path: path.resolve(__dirname, "./dist"),
    4  publicPath: "dist/",
    5},
  • there are cases where you have to specify an exact url

    12 Asset/inline module type

  • generates data URI
  • doesn't output new file in output directory
  • uses base64 representation of the file
    • for small asset files: svg
  • changing our type:
1 {
2        test: /\.(png|jpg)$/,
3        type: "asset/inline",
4      },
  • we get the following in our dev tools:
1<img alt="Kiwi" width="300" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/..." />
  • i.e, an enormous string that represents the image, about 277,000 characters long!
  • this string is embedded in our bundle.js file
  • there are some use cases where using asset/inline is better than asset/resource
  • with asset/resource, webpack generates a separate file for every image you are using
    • this means the browser must make a separate http request for every image it displays
    • the extra requests make sense if the files are huge jpgs, but not if they are small SVG icons
      • better to use asset/inline to save making more http requests

13 General asset type

  • combination of inline and resource type
  • we change our type to asset:
1{
2        test: /\.(png|jpg)$/,
3        type: "asset",
4      },
  • webpack decides whether to choose inline or resource based on file size (8kb default)
  • changing the 8kb rule:
1{
2        test: /\.(png|jpg)$/,
3        type: "asset",
4        parser: {
5          dataUrlCondition: {
6            maxSize: 3 * 1024, // 3 kilobytes
7          },
8        },
9      },
  • we use the maxSize property of the dataUrlCondition property of parser to set the size that webpack uses to decide between asset/resource and asset/inline

14 asset/source module type

  • reads the contents of the file into a JavaScript string and injects into JavaScript bundle as is without modifying
  • similar to asset inline - no new file generated to output directory

  • we add an altText.txt file containing text:

1Kiwi alt text
  • import in add-image
  • set our image alt text to altText:
1img.alt = altText;
  • updated webpack.config:
1 {
2        test: /\.txt/,
3        type: "asset/source",
4      },
  • and our element in the browser has our new alt text:
1<img alt="Kiwi alt text" width="300" src="dist/23de234a71129d9c860b.jpg">

15 What is webpack loader?

  • allow you to import other types of files that you can't import with asset modules
  • webpack designed to bundle all dependencies into one or more files
  • dependencies are usually JavaScript modules that main file requires to works
  • with webpack loaders you can import more stuff:
    • css files
    • SASS
    • LESS
    • Handlebars (semantic templates - I assume this is semantic html templates?)

16 Handling CSS with webpack

  • why import CSS?
    • typical with React, for example, to separate components and styles into many files
  • I'm not sure I follow the point being made here, he says that it's much better when behaviour (JavaScript) styles (css) are in the same place
    • he says it's easier to fix bugs, add features, and reuse these components
    • not sure how this applies to webpack, it's minifying and importing everything into JavaScript, but we're not editing the bundle.js, moving on ...
  • here we are going to create a new components folder, and inside that we create a hello-world-button folder
  • we move our hello-world.js file into that folder, renaming it to hello-world-button.js
  • the contents of hello-world-button.js look like:
1class HelloWorldButton {
2  render() {
3    const button = document.createElement("button");
4    button.innerHTML = "Hello World";
5    const body = document.querySelector("body");
6    body.appendChild(button);
7  }
8}
9
10export default HelloWorldButton;
  • we update our index.js:
1import HelloWorldButton from ".components/hello-world-button/hello-world-button.js";
2
3const helloWorldButton = new HelloWorldButton();
4helloWorldButton.render();
  • running npm build, we can see our button
  • we add an onclick
  • we add some css classes to our button and paragraph
  • we create a CSS file inside our button folder
  • now we need to update our config file so webpack can import the CSS
1 {
2        test: /\.css$/,
3        use: ['style-loader', 'css-loader']
4      },
  • here we are telling webpack that when it imports CSS files, it must use style-loader AND css-loader
  • css loader reads the css file and returns the contents
  • style-loader takes the css and injects it into the page using style tags
    • it also bundles the css and JavaScript into single file
  • when using loaders, you have to install them explicitly with npm install
  • we install as dev deps
  • looking in the dev tools, we can see that we have our css style tag in the head tag and everything is working

17 Handling SASS

  • we change our css file extenson to scss
  • our new scss file:
1$font-size: 3rem;
2$button-background-color: green;
3$button-font-color: white;
4$text-font-color: red;
5
6.hello-world-button {
7  font-size: $font-size;
8  padding: 0.5rem 1rem;
9  background-color: $button-background-color;
10  color: white;
11  outline: none;
12}
13
14.hello-world-text {
15  font-size: 3rem;
16  color: $text-font-color;
17  font-weight: bold;
18}
  • our scss rule:
1 {
2        test: /\.scss$/,
3        use: ["style-loader", "css-loader", "sass-loader"],
4      },
  • the order of the loaders in the array is important, webpack processes loaders from right to left
  • sass-loader is invoked first, converts SASS to CSS
  • then invokes css loader, takes output from sass-loader and converts to JavaScript representation
  • finally, style loader is invoked which creates style tags inside our html page and places css into it
  • must install sass-loader and must also install dart-sass (which confusingly has been named to just 'sass')
  • npm run build and everything looks good

18 using the latest JavaScript features with Babel

  • why do we need another loader to load JavaScript files?
  • JavaScript is based on ecmascript specification
    • evolving all the time
  • when new features are released in ecmascript, browsers implement new features
  • takes time to implement new features in all browsers
  • developers want to use the new features immediately, and not wait until all browsers have implemented the new features
  • you can use special tools to use the new features right away
    • tools convert modern JavaScript code into older JavaScript code that is supported by all browsers
    • one of these tools is called Babel
  • Babel
    • the most popular JavaScript compiler
  • demonstrating the usefulness of Babel (this example may be out of date but it conveys the idea)

    • we add a property to our HelloWorldButton class
    1buttonCssClass = "hello-world-button";
    • we reference that property when we add the class in our render()
    1button.classList.add(this.buttonCssClass);
    • buttonCssClass is a class property
      • not supported by major browsers (at time when course created)
      • most browsers only allow methods inside JavaScript classes
      • this would throw an error in the past if we tried to npm run build
  • we just use a loader, babel loader for this
  • new rule in config:
1{
2        test: /\.js$/,
3        exclude: /node_modules/,
4        use: {
5          loader: "babel-loader",
6          options: {
7            presets: ["@babel/env"],
8          },
9        },
10      },
  • our new rule applies to all js files
  • it excludes files in node_modules
  • it uses the babel-loader
    • we can set options for any loaders in webpack
  • the @babel/env preset compiles all ecmascript 6, 7, 8, 9 etc down to ecmascript 5
    • in other words, env preset supports latest javascript standard defined in latest ecmascript specification
  • we also need a babel plugin to support class properties, since they are not part of official ecmascript specification
1plugins: ["@babel/plugin-proposal-class-properties"],
  • if you find a modern JavaScript feature that isn't supported by major browsers, it should be possible to find a babel plugin for it
  • we need to install some babel stuff
    • @babel/core
    • babel-loader
    • @babel/preset-env
    • @babel/plugins-proposal-class-properties
  • our new rule in webpack config:
1{
2        test: /\.js$/,
3        exclude: /node_modules/,
4        use: {
5          loader: "babel-loader",
6          options: {
7            presets: ["@babel/env"],
8            plugins: ["@babel/plugin-proposal-class-properties"],
9          },
10        },
11      },
  • webpack now uses babel-loader to import JavaScript files
    • if we use a cutting edge feature not included in the current ecmascript spec, it compiles it to older JavaScript

19 Experimental JavaScript features

  • viktor acknowledges that class properties are already supported by browsers:

Experimental JavaScript Features In the previous video we were talking about enabling support for Experimental JavaScript features in your application.

In this particular video we were using Class Properties as an example of such experimental feature. However, I would like to mention that all major Browsers already added support for Class Properties. Nowadays this feature works out-of-the-box, and you don't have to include @babel/plugin-proposal-class-properties separately in your Webpack configuration.

However, in the previous video I am showing a general approach for handling any kind of non-standard JavaScript features (not only Class Properties). For example, if you want to use pipeline operator |> in your code, then you still need to configure Babel for your application as well as add a special Babel plugin named @babel/plugin-proposal-pipeline-operator that adds support for this feature.

At some point in the future major Browsers will add support for Pipeline operator feature as well, and you won't have to enable it separately. However, there will always be new features (not supported out-of-the-box) that you can enable using the approach described in the previous video.

Have a nice day,

Viktor

20 What is webpack plugin?

  • webpack plugins are JavaScript libraries that do everything loaders can't do (loaders are for importing really)
  • they modify how the bundles themselves are created
    • example, uglifyJSPlugin takes bundle.js and minifies it
  • add plugins using:
1plugins: [new PluginName()];
  • plugin use cases
    • define global constants across the application
    • minify bundle
    • generate other files in addition to bundle.js

21 Minification of resulting webpack bundle

  • why minify the bundle?
    • websites load faster
    • smaller bundle = faster loading
    • less data usage
  • current bundle size is 20.7Kb
  • now let's try to reduce this
  • we add a new section in our config called plugins
  • we use terser plugin
  • when you use a new webpack plugin, you have to install it
    • terser plugin is already installed with webpack from webpack 5
  • plugins need to be imported:
1const TerserPlugin = require("terser-webpack-plugin");
  • we add it to our config:
1 plugins: [new TerserPlugin()],
  • running webpack, our bundle size is now 5.95Kb - that is a big reduction!

22 extracting css into a separate bundle with mini-css-extract-plugin, part 1

  • up to this point, we have been using css-loader and style-loader to place our css in a style tag in the head of our html document
  • this is fine for development, but not for production
    • the problem is our bundle will become very large as our application grows
  • we can extract our css into a separate file that is generated in addition to bundle.js
    • we'll have two bundles instead of one
  • why is this better?
    • size of JavaScript bundle is smaller, faster to download
    • can load multiple files in parallel
  • add a new plugin to webpack config, mini-css-extract-plugin, note how we specify the name of the file
1 plugins: [
2    new TerserPlugin(),
3    new MiniCssExtractPlugin({ filename: "styles.css" }),
4  ],
  • to use this plugin, we need to modify our rules for CSS and SASS
1{
2        test: /\.css$/,
3        use: [MiniCssExtractPlugin.loader, "css-loader"],
4      },
5      {
6        test: /\.scss$/,
7        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
8      },
  • remember, have to install and import our new plugin
  • to use styles.css, we need to add it to our html code in index.html
1 <link rel="stylesheet" href="./dist/styles.css">

23 extracting css into a separate bundle with mini-css-extract-plugin, part 2:

  • examining styles.css
    • this contains all styles from the application
  • here, we create another component and some styles, to make sure all our styles are actually dumped into styles.css
  • running npm run build and everything works fine - the styles for our new component are in styles.css

24 browser caching

  • all the assets need to be downloaded on every refresh and it's a disaster 😀
  • if the file doesn't change between reloads, browser can cache it or save it
    • when you reload the page, assets taken from cache
  • can lead to another issue
    • what if the assets have changed (bugfix)?
    • need a mechanism to update the cache
  • popular approach to updating cache is creating a new file with a new name every time the code gets changed
    • browsers remember files by name
    • if name changes, browser will download new version
    • webpack can automatically change our file name every time we change our code
  • can add md5 hash to the end of the file
    • this way we get a new file name each time
    • if you change css file, it will get a new name, but JavaScript file will retain its old name
    • during page reload, new css will be fetched but unchanged JavaScript file will be retrieved from cache
  • we can add md5 hash to our file names like so:
1module.exports = {
2  entry: "./src/index.js",
3  output: {
4    filename: "bundle.[contenthash].js",
5    path: path.resolve(__dirname, "./dist"),
6    publicPath: "dist/",
7  }, ...
  • we simply add .[contenthash] to the filename
  • running webpack we get our new file name:
1bundle.74eb891a0a73f1921d78.js
  • if you change something in the JavaScript code, you will end up with a different file name
  • this works for css files as well

25 Cleaning dist folder before generating new bundles

  • clean webpack plugin is what we use
  • install, import in config and add to the plugin array
  • using this plugin, all files are removed from the dist folder every time we run webpack
  • possible to clean multiple folders this way
    • provide options when instantiating the plugin
1
2
  • cleanOnceBeforeBuildPatterns removes old files before webpack generates new files
  • can specify an array of the file patterns to remove
  • all patterns are relative to the webpack output path directory
    • for us, this is the dist directory
1"**/*";
2// remove all files and subdirectories inside output.path
3// this is the default behaviour
  • to remove files outside output.path, specify an absolute path for the directory to clean
1path.join(process.cwd(), "build/**/*");
2// removes all files and subfolders inside build folder
  • our plugin setup updated:
1 new CleanWebpackPlugin({
2      cleanOnceBeforeBuildPatterns: [
3        "**/*",
4        path.join(process.cwd(), "build/**/*"),
5      ],
6    }),
  • npm run build and our folders (build and dist) have been cleaned out as expected
  • another way to achieve this:
    • use webpack config option instead of clean-webpack-plugin
  • the config option:
1
2 output: {
3     filename: '[name].bundle.js',
4     path: path.resolve(__dirname, 'dist'),
5    clean: true,
6   },
  • we can just set clean to true, or we can use an object with two properties
1 clean: { dry: true,
2    keep: /\.css/ },
3  },
  • dry: true means tell me what would be removed, but don't remove anything
  • keep css, obviously keeps files that match this pattern
  • webpack logs files to be kept/removed when we make changes and run webpack:
1LOG from webpack.CleanPlugin
2<i> styles.b352de4e05432220c759.css will be kept
3<i> bundle.74eb891a0a73f1921d78.js will be removed
  • having changed some css and some JavaScript, it's worth noting that after running webpack we have 2 style files and 2 bundles
  • the output clean option webpack only supports dry and keep
  • clean-webpack-plugin let's us do many extra things like remove files from folders other than path.output

26 Generating html files automatically during webpack build process

  • our site is now broken as we have added md5 hash to our file names, but we haven't updated our index.html
  • webpack has a plugin to update the names of the files in our html after each build
    • can also create html files for us
  • we add the plugin, import and install via npm
1new HtmlWebpackPlugin(),
  • running npm run build
    • webpack now generates an index.html file for us with correct names for our css and js files
  • the path to our bundles still starts with "dist/"
    • need to change the publicPath from 'dist/' to an empty string in config
  • we can delete our original index.html
    • we'll just use the file generated by webpack

27 Customizing generated html files

  • we'll pass custom options to it
  • webpack changed the title of the page, now it's 'webpack app'
  • we want our title set to 'hello world'
  • example custom options:
1 new HtmlWebpackPlugin({
2      title: "Hello World",
3      filename: "subfolder/custom_filename.html",
4      meta: {
5        description: "some description",
6      },
7    }),
  • title is the title
  • filename, here we can specify the output file name and the directory to place it in
  • meta, here we specify additional meta tags
  • there are a lot more options we can set with this plugin (see its github page)

28 Integration with Handlebars

  • here we will create our own template for generating html files
  • we need a template engine for this
  • lots to choose from: pug, ejs, underscore, handlebars, html-loader etc
  • here we will use Handlebars
    • template engine for JavaScript
    • lets you separate business logic from presentation
  • when to use template engine?
    • if you are generating html inside your JavaScript
  • in our src folder, create a template called index.hbs
    • hbs = handlebars templates
    • copy the html from our generated index.html into our template
    • remove the script and link tag for our css and js bundles
      • webpack will add these during build
  • we change the description and title to use Handlebars' variables
  • the description:
1<meta name="description" content="{{htmlWebpackPlugin.options.description}}" />
  • the title:
1<title>{{htmlWebpackPlugin.options.title}}</title>
  • we tell webpack to use our template
1 new HtmlWebpackPlugin({
2      title: "Hello World",
3      template: "src/index.hbs",
4      description: "some description",
5    }),
  • note, description is no longer nested inside the meta property
  • we must tell webpack how to use .hbs files:
1 {
2        test: /\.hbs$/,
3        use: ["handlebars-loader"],
4      },
  • need to install the loader and handlebars!

29 More webpack plugins

  • This is just an overview of what we did so far and highlights that there are a lot of plugins available and third-party plugins

30 Production versus development builds

  • prod requires different set up than development builds
  • production
    • website should be fast as possible
    • bundles be as small as possible
  • in development
    • might want to see additional things in our JavaScript code, like sourcemaps
  • in the following lessons, we'll learn about differences in prod and dev builds and how to make webpack config serve both use cases

31 Mode

  • we have an option called mode in webpack config
    • enables built-in optimizations for production and dev builds
  • we've been using mode none so far
  • three values
    • none, development, production
  • production enables a lot of plugins, including TerserPlugin
  • mode sets the process.env.NODE_ENV variable
    • can use this to check if we are in production or development mode
  • production and development mode handle errors differently
    • to create an error, we invoke a method that doesn't exist
  • if we npm run build and check in browser console, we see an error
    • if we click on the error, we see the minified JS in our bundle and it is hard to figure out the error
  • we change to development mode and npm run build
  • now we can see where the error is in our code in the console:
1index.js:13 Uncaught TypeError: helloWorldButton.methodThatDoesNotExist is not a function
2    at eval (index.js:13:18)
3    at ./src/index.js (bundle.32b57c306c549e472356.js:2:6178)
4    at __webpack_require__ (bundle.32b57c306c549e472356.js:2:7910)
5    at bundle.32b57c306c549e472356.js:2:8389
6    at bundle.32b57c306c549e472356.js:2:8428
  • this works because development mode uses source maps by default

32 Managing webpack config for production and development use cases

  • we can manage our config for production and development by simply creating two separate configuration files, one of each
  • we rename our webpack.config to webpack.production.config
  • we create a new file: webpack.dev.config
  • copy contents of production to dev
  • in production:
    • can remove TerserPlugin as it is included by default
    • and remove the import
  • in development config:
    • remove [contenthash]
    • remove Terser, no need to minify during development
      • takes time, makes sense in production to reduce page load time
      • our aim in dev is to reduce build time to optimize developer's experience
      • remove MiniCssExtractPlugin from plugins
      • can just use style-loader for css, old:
1{
2        test: /\.css$/,
3        use: [MiniCssExtractPlugin.loader, "css-loader"],
4      },
5      {
6        test: /\.scss$/,
7        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
8      },
  • dev config, new:
1 {
2        test: /\.css$/,
3        use: ["style-loader", "css-loader"],
4      },
5      {
6        test: /\.scss$/,
7        use: ["style-loader", "css-loader", "sass-loader"],
8      },
  • in a large project, these changes in our config will make things faster
    • not much difference for small projects
  • now we need two scripts, one for production and one for development
  • in package.json:
    • currently we have one script:
1"scripts": {
2    "test": "echo \"Error: no test specified\" && exit 1",
3    "build": "webpack"
4  },
  • we specify the config to use:
1 "scripts": {
2    "test": "echo \"Error: no test specified\" && exit 1",
3    "build": "webpack --config webpack.production.config.js"
4  },
  • now we add a new script to run our dev build config
  • testing our scripts, they both work fine

33 Faster development with webpack dev server

  • ideally in development, we want to see our changes in the browser instantly without building our code
    • can do this with webpack dev server
    • install with npm i webpack-dev-server
    • add config option in dev mode:
1 devServer: {
2    port: 9000,
3    static: {
4      directory: path.resolve(__dirname, "./dist"),
5    },
6    devMiddleware: {
7      index: "index.html",
8      writeToDisk: true,
9    },
10  },
  • many options for devServer
    • port is self explanatory
    • static is the directory to serve from
    • index is the main file to serve
    • writeToDisk - by default webpack dev server serves files from memory and doesn't write the output files to disk
  • have to update our package.json to point it to webpack dev server
1  "dev": "webpack serve --config webpack.dev.config.js --hot"
  • '--hot' enables hot module replacement
    • by simply saving the file, we can see updates in our browser without running our build script
  • running our dev build, we see everything is good and our app is available at localhost on port 9000

34 cleaning up

  • here we revert some changes that viktor made in previous lesson.

35 introduction, multiple page applications

  • up to now we've been dealing with a single page application (index.html)
  • now we look at multiple page applications
    • for example, our hello world page and our kiwi page
    • these pages may have something in common, example, lodash library
    • if two or more pages have common dependencies (they use the same code), we need to figure out how to handle that with webpack
  • in this section, we learn how to split our JavaScript code into mutliple bundles + how to create multiple html files for different pages of our website

36 creating kiwiimage component

  • imagine our webiste has two pages
    • hello world page
    • 2nd one is kiwi page
  • second page would need a new component that adds a kiwi image to the page
  • we create our KiwiImage component:
1import Kiwi from "./kiwi-image.jpg";
2
3class KiwiImage {
4  render() {
5    const bodyDomElement = document.querySelector("body");
6    const img = document.createElement("img");
7    img.src = Kiwi;
8    img.alt = "Kiwi";
9    img.classList.add("kiwi-image");
10    bodyDomElement.appendChild(img);
11  }
12}
13
14export default KiwiImage;
  • we create a kiwi-image.scss file, import it into our kiwi-image.js

37 Code splitting in webpack: multiple JS and CSS bundles

  • we'll create multiple bundles and include them on the appropriate pages (hello world or kiwi)
  • creating the second page (kiwi), we create kiwi.js
  • our kiwi.js:
1import KiwiImage from "./components/kiwi-image/kiwi-image";
2import Heading from "./components/heading/heading";
3
4const heading = new Heading();
5const kiwiImage = new KiwiImage();
6heading.render();
7kiwiImage.render();
  • we rename our index.js to hello-world.js
  • our hello-world.js and kiwi.js represent two different entry points to be included in two different html pages
  • tell webpack to create two separate JavaScript bundles from those two files

    • in webpack production config we replace this:
    1entry: "./src/index.js",
    • with this:
    1 entry: {
    2  "hello-world": "./src/hello-world.js",
    3  "kiwi": "./src/kiwi.js",
    4},
    • we have to update the output settings in config
    • we can tell webpack to use names we specify in our entry points for the output files like so:
    1output: {
    2  filename: "[name].[contenthash].js",
    3  path: path.resolve(__dirname, "./dist"),
    4  publicPath: "",
    5},
    • note we replaced 'bundle' with [name]
      • webpack will use kiwi and hello-world from our entry point names to name the output files
  • common substitutions are: [name], [contenthash], and [id]
  • [id] inserts the internal chunk ID in the name of the generated bundle
    • not human readable/friendly
  • it's also possible to define a function that will return the file name
  • can do the same for CSS files
    • we can't change the names of our generated CSS in output
  • CSS filename can be changed in the MiniCssExtractPlugin plugin options:
1  plugins: [
2    new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" }),
3    ... ]
  • running npm run build, we generate a bunch of files with names as expected with md5 hash etch, all is well

38 How to generate multiple HTML files

  • in lesson 37, we generated different bundles, but these are all now being included in a single html file - index.html
  • it's possible to disable minification of html like so:
1 new HtmlWebpackPlugin({
2      title: "Hello World",
3      template: "src/index.hbs",
4      description: "some description",
5      minify: false, // default true in production
6    }),
  • here we learn how to generate different html files and include different bundles in each
  • to generate two html files, we need to include the HtmlWebpackPlugin twice in our plugin array
  • we add different file names
  • we rename our index.hbs template to something more generic, page-template.hbs
1 new HtmlWebpackPlugin({
2      filename: "hello-world.html",
3      title: "Hello World",
4      template: "src/page-template.hbs",
5      description: "hello world",
6      minify: false, // default true in production
7    }),
8    new HtmlWebpackPlugin({
9      filename: "kiwi.html",
10      title: "kiwi",
11      template: "src/page-template.hbs",
12      description: "kiwi",
13      minify: false, // default true in production
14    }),
  • how do we know which bundles to include in each html file?
    • HtmlWebpackPlugin has an option for this called 'chunks'
    • we can specify which bundles to include where using chunks option:
1chunks: ["hello-world"],
  • chunk names are taken from the entry point object
  • running build process we get a kiwi.html file and a hello-world.html file & they have appropriate css/js files

39 extracting common dependencies while codesplitting

  • here we learn how to handle common deps between multiple bundles
  • sometimes different pages depend on a common library or framework
    • you don't want the library to be included in every generated bundle
  • imagine you want to show the name of the page in the heading
    • need to modify heading component
    • in our heading.js we modify the h1 innerhtml: js class Heading { render(pageName) { const h1 = document.createElement("h1"); h1.innerHTML = "Webpack is awesome. This is '" + pageName + "' page."; const body = document.querySelector("body"); body.appendChild(h1); } }
  • we've added pageName
  • since we added the pageName parameter, we will need to change all invocations of the function
    • in hello-world.js
    • in kiwi.js
  • in hello-world.js
1heading.render("hello world");
  • we use lodash to capitalize the first letter of 'hello world'
    • now we see where this is leading - lodash dep in two files
  • updated kiwi.js:
1import KiwiImage from "./components/kiwi-image/kiwi-image";
2import Heading from "./components/heading/heading";
3import _ from "lodash";
4const heading = new Heading();
5const kiwiImage = new KiwiImage();
6heading.render(_.upperFirst("kiwi"));
7kiwiImage.render();
  • install lodash
  • update hello-world.js to use lodash
  • npm run build
  • our bundles are now bigger because they both contain lodash
  • webpack can extract common deps like lodash into its own bundle
  • in webpack production config we add an optimization option:
1 mode: "production",
2  optimization: {
3    splitChunks: {
4      chunks: "all",
5    },
6  },
  • npm run build again
  • now we have a new bundle that contains our lodash
    • this will be cached separately
    • this bundle will be included in all html pages that depend on this bundle
  • we can remove lodash from kiwi.js, for example, and we see that the lodash bundle is not included in the kiwi.html file
  • worth noting that if lodash has not changed between your deployments, the lodash bundle is cached and will not be redownloaded by users of your website

    • if lodash has changed between your website deployments, then users will need to redownload it
    • bottom line, users will not have to download so much code every time they visit your site
    • can use this mechanism to extract React into a separate bundle even if you don't have multiple pages

      • useful for browser caching as well

40 setting custom options for code splitting

  • here we will do what we did above (extracting common deps into a separate bundle), but with React
  • remove lodash and import react
  • install react
  • npm run build
  • we see webpack has not extracted react to a separate bundle
    • this is because webpack only extracts shared deps to a separate bundle where they exceed 30kb before minification
    • react is less than 30kb
  • we can make sure React is extracted to its own bundle by changing the default 30kb number
  • we add minSize:
1 optimization: {
2    splitChunks: {
3      chunks: "all",
4      minSize: 3000,
5    },
6  },
  • run build and we see:
1 asset 294.c594dd3943baf191f5c8.js 1.37 KiB [emitted] [immutable] [minimized]
  • which is our react bundle

41 How to set up dev environment for multi page applications

  • we update the entry property in our dev config (same as production)
  • our output:
1filename: "[name].bundle.js",
  • we just use name here, not contenthash
    • no need to worry about caching in dev
  • add a second HtmlWebpackPlugin (like we did in production)
1  new HtmlWebpackPlugin({
2      filename: "hello-world.html",
3      chunks: ["hello-world"],
4      title: "Hello World",
5      template: "src/page-template.hbs",
6      description: "Hello World",
7    }),
8    new HtmlWebpackPlugin({
9      filename: "kiwi.html",
10      chunks: ["kiwi"],
11      title: "Kiwi",
12      template: "src/page-template.hbs",
13      description: "Kiwi",
14    }),
  • note, we don't have to split deps into a separate bundle in dev
    • in dev we care about the performance of the build process, not the performance of the application
  • at this point we have good webpack configurations for both production and development

42 How to use github repository

  • you don't have to use the repo, only if stuck
  • you can clone to your computer
  • already done this
  • there are two branches for each lesson - starting branch and ending branch
  • can get a list of all branches using:
1git branch -a
  • you checkout a branch you are interested in working on
1git checkout -b [branchname]
  • you can discard changes before switching branch:
1git reset --hard HEAD

43 Integration with Express framework

  • now we look at how to use webpack with a backend
  • you can use any technology, php, node, java etc to create backend
  • we'll use nodejs and express here
  • we'll look at SPAs and multiple page apps

44 Getting code for SPA

  • reiteration of steps of previous video as a slide

45 integrating express into our app

  • this is for a single page application
  • we will create simple web server using nodejs and express
  • create server.js inside src
  • our server.js:
1import express from "express";
2
3const app = express();
4
5app.get("/", function (req, res) {
6  res.send("some dummy content");
7});
8
9app.listen(3000, () => {
10  console.log("app is listening on port 3000...");
11});
  • install express
  • add a new script to run server:
1"start": "node src/server.js"
  • now we run it, npm start
  • we can view it on localhost:3000

46 Serving html via express

  • here we replace this:
1res.send("some dummy content");
  • with the contents of a html file that we read into a variable, and send that back to the browser
  • require path
  • we get the path to the file we want to serve:
1const pathToHtmlFile = path.resolve(__dirname, "../dist/index.html");
  • our server.js looks like:
1const express = require("express");
2const app = express();
3const path = require("path");
4const { readFileSync } = require("fs");
5
6app.get("/", function (req, res) {
7  const pathToHtmlFile = path.resolve(__dirname, "../dist/index.html");
8
9  const contentOfHtmlFile = readFileSync(pathToHtmlFile, "utf-8");
10  res.send(contentOfHtmlFile);
11});
12
13app.listen(3000, function () {
14  console.log("Application is running on http://localhost:3000/");
15});
  • we rebuild and start the server
  • we have an empty page, why?
  • in the browser console we see that it can't load our CSS and JavaScript because of MIME types
    • this happens because we didn't tell Express how to load our assets
  • Express correctly sends back the html file
    • the browser then requests the JavaScript and the CSS, but Express doesn't know what to do with these requests because we haven't specified that

47 Handling JS and CSS via Express

  • we want to send a css file back to browser everytime we get a request for a css file!
    • same for all static files
  • we create a route for static files
1app.use("/static", express.static(path.resolve(__dirname, "../dist")));
  • our route is /static
    • all requests to /static go to our /dist folder
  • now we need to add /static to all our static assets (css, js, images etc)
  • in our index.html, our links look like:
1<link href="styles.058b997e46e350b6336b.css" rel="stylesheet">
  • there is no /static at the moment
    • we can just change our webpack config to add this
1  output: {
2        filename: 'bundle.[contenthash].js',
3        path: path.resolve(__dirname, './dist'),
4        publicPath: '/static/'
5    },
  • now our assets have /static and Express knows how to serve our assets:
1<script defer="defer" src="/static/bundle.6ed7dc912b0752c9e619.js"></script>
2<link href="/static/styles.058b997e46e350b6336b.css" rel="stylesheet">

48 Getting code for multiple page application

  • just a reminder to checkout the branch and install deps

49 integrating Express.js into a multiple page application

  • our updated server.js file
    • we add an extra route, one for each page
1const path = require("path");
2const { readFileSync } = require("fs");
3const express = require("express");
4const app = express();
5
6app.get("/hello-world/", (req, res) => {
7  const pathToHtmlFile = path.resolve(__dirname, "../dist/hello-world.html");
8  const contentOfHtmlFile = readFileSync(pathToHtmlFile, "utf-8");
9  res.send(contentOfHtmlFile);
10});
11app.get("/kiwi/", (req, res) => {
12  const pathToHtmlFile = path.resolve(__dirname, "../dist/kiwi.html");
13  const contentOfHtmlFile = readFileSync(pathToHtmlFile, "utf-8");
14  res.send(contentOfHtmlFile);
15});
16
17app.use("/static", express.static(path.resolve(__dirname, "../dist")));
18
19app.listen(3000, () => {
20  console.log("app listening on port 3000...");
21});

50 Creating two separate applications, part 1

  • what we're doing now is separating our kiwi and hello-world into two separate applications
  • each will have its own webpack configuration & different dependencies
  • later on, we'll set up module federation between these two apps
  • create a new folder and move files
    • 'hello-world' folder
  • move our readme, package lock, package.json, src folder and our dev and production webpack configurations into the hello-world folder
  • create another folder called kiwi
    • copy the contents of hello-world into kiwi
  • now the contents of both folders are the same
  • we can update each folder (app) to get rid of things that the app doesn't require
  • edit the webpack dev config
    • currently two entry points, but we only need one
  • delete all the files required by kiwi from hello-world, and vice versa
  • since we have two applications, we'll have to change the ports that each is served on
    • dev port will be 9000 for hello-world and 9001 for kiwi
  • we change the name of the file from index.html in both:
1devMiddleware: {
2      index: "kiwi.html",
3      writeToDisk: true,
4    },
  • we can remove one HtmlWebpackPlugin from each dev config:
1  new HtmlWebpackPlugin({
2      filename: "hello-world.html",
3      chunks: ["hello-world"],
4      title: "Hello world",
5      description: "Hello world",
6      template: "src/page-template.hbs",
7    }),
  • hello world doesn't import images, so we can dispense with:
1 {
2        test: /\.(png|jpg)$/,
3        use: ["file-loader"],
4      },
  • can remove .css rule since we are using scss everywhere
  • we don't need to specify chunks in our HtmlWebpackPlugin config for our hello-world webpack config
1  chunks: ["hello-world"],
  • now we can do all of this for production config as well
  • next, update server.js so we can run the app in production mode
    • we don't need the kiwi route in hello-world and vice versa
  • we can update the route so it points to the root of the application
  • instead of "/hello-world/":
1app.get("/", (req, res) => {...})
  • we also update the ports in our server.js file, 9000 for hello-world and 9001 for kiwi
  • I adjusted both applications in this lesson (viktor just adjusted the hello-world application)

51 creating two separate applications, part two

  • we don't use class properties so can remove this from kiwi:
1   plugins: ["@babel/plugin-proposal-class-properties"],
  • now we can cd into each app and npm run build and see what happens
  • hello-world is working fine on port 9000
  • having a problem with kiwi app!
  • issue resolved, was the get route in server.js
1app.get("/", (req, res) => {...})
  • still had '/hello-world/' in hello-world app, but kiwi wasn't accessible on localhost:9002/kiwi because I had correctly updated it to app.get('/')

52 setting up module federation

  • module federation allows one app to dynamically load modules from another app at runtime
  • our kiwi and hello-world apps were set up independently so they can be developed independently by two different teams
    • have different dependencies
    • can be deployed separately
  • now, we will take the HelloWorldButton defined in hello-world and reuse it inside kiwi
  • module federation is availble in webpack starting from version 5
  • in webpack dev config we can set up module federation, in hello-world dev config:
1const { ModuleFederationPlugin } = require("webpack").container;
  • and:
1 new ModuleFederationPlugin({
2      name: "HelloWorldApp",
3      filename: "remoteEntry.js",
4      exposes: {
5        "./HelloWorldButton":
6          "./src/components/hello-world-button/hello-world-button.js",
7      },
8    }),
  • name is the name of the application
  • when building, webpack generates a file that contains everything this application exports to out world so other apps can use the contents
    • the convention is to call this file 'remoteEntry.js'
  • exposes is a list of modules exposed by the application
    • name of module and the path to the module
  • other apps will reference this app using the public url
    • this public url is baked into remoteEntry file
  • during build, webpack doesn't know where we will deploy our app
    • we have to tell webpack what url we are using
    • we do this by changing the publicPath property to the public url
1  publicPath: "http://localhost:9001",
  • we update the production config similarly
  • we change the public url
    • we had been using a publicPath of '/static' but we're changing this
  • we have to update our server.js app.use:
1app.use("/", express.static(path.resolve(__dirname, "../dist")));
  • in kiwi app, we need to configure module federation plugin to consume the hello button component
  • import module federation
  • add the plugin and options
1 new ModuleFederationPlugin({
2      name: "KiwiApp",
3      remotes: {
4        HelloWorldApp: "HelloWorldApp@http://localhost:9001/remoteEntry.js",
5      },
6    }),
  • we name our kiwi app
  • we add remotes, which is a list of apps/modules that our app can consume
    • we don't specify exactly what modules we will consume here
  • we do the same in production

53 consuming federated modules

  • we import hello world button into kiwi.js
  • we use import()
    • The import() syntax, commonly called dynamic import, is a function-like expression that allows loading an ECMAScript module asynchronously and dynamically into a potentially non-module environment.
1import("HelloWorldApp/HelloWorldButton").then((HelloWorldButtonModule) => {
2  const HelloWorldButton = HelloWorldButtonModule.default;
3  const helloWorldButton = new HelloWorldButton();
4  helloWorldButton.render();
5});
  • we used export default in hello-world-button.js, so we need to get the default export with '.default'
  • here we are using a component that is defined in another application!
  • getting an error in importing chunk 568, must be issue in kiwi
  • still can't resolve ChunkLoadError
  • fixed issue with ChunkLoaderError

    • i was missing a trailing slash in publicPath for hello-world:
    1  publicPath: "http://localhost:9001/",
    • in browser dev tools for kiwi app in the network tab we can see the remoteEntry.js file that is loaded from localhost:9001 (hello world)
    • should note that this module federation allows us to consume the HelloWorldButton component in our kiwi app from the hello-world app without adding it as a dependency in package.json

    54 Modules are loaded at runtime

    • here we will see another benefit of module federation
    • assuming we need to change our button component due to new business requirements
      • we'll just change the color of the button, to blue
      • we change style of button in hello-world app
      • rebuild hello-world app and start it
  • the button color changes in both apps, but we haven't altered code in kiwi app

    • didn't even have to restart kiwi!
    • this is because module federation loads the button dynamically at runtime

55 creating micro frontends

  • in the following videos we'll build
    • a nav bar with two items
    • one item will lead to hello world page
    • second item will lead to kiwi page
    • under the nav bar, we'll show the contents of the respective pages
    • about micro front ends
      • benefits
        • Scalability. The micro frontends framework allows applications to be broken down into smaller, independent units. This promotes greater scalability as teams can focus on developing specific components without comprehending the entire codebase.
        • Autonomous development. With micro frontends, different teams can work independently on separate components using the technology stack best suited for their component's requirements. This autonomy facilitates parallel development, reducing project timelines and increasing overall productivity.
        • Ease of deployment. Given its independent nature, the framework enables more frequent and isolated deployments. This minimizes the risk of deploying changes to an extensive, monolithic application and facilitates quicker feature roll-outs.
        • Improved testing. Isolation of components also enhances the testing process. Each micro frontend can be individually validated, resulting in fewer bugs, higher quality code, and a more reliable application.
        • Modernization. By allowing each component to be updated or replaced independently, the micro frontends architecture enables an organization to modernize its applications incrementally. This reduces the risks associated with significant overhauls and allows organizations to adopt new technologies at their own pace.
  • our micro frontend will consist of:
    • a dashboard that has the nav bar
    • the content of the page (under nav bar) will be a separate application loaded from a different url
    • the dashboard application will providing the routing between hello world and kiwi content
    • this is called micro frontend architecture
    • useful when each page is developed by a different team
    • happens if company is huge and each page is complex
      • viktor said he worked for such a company and there were more than 1000 software developers working on the same application
        • 100 teams
        • each team responsible for one page of the app!
  • need to prepare our apps so they can be used as micro frontends
  • hello world consists of a heading and a button
    • we export all of this as a component so we can consume it in the dashboard application
    • create a new component in the components folder of hello-world
      • hello-world-page/hello-world-page.js
      • copy everything from hello-world into this new file
  • our hello-world-page.js file:
1import HelloWorldButton from "../hello-world-button/hello-world-button.js";
2import Heading from "../heading/heading.js";
3
4class HelloWorldPage {
5  render() {
6    const heading = new Heading();
7    heading.render();
8    const helloWorldButton = new HelloWorldButton();
9    helloWorldButton.render();
10  }
11}
12export default HelloWorldPage;
  • we expose our component to outer world
    • add the component to list of exposed modules
1 new ModuleFederationPlugin({
2      name: "HelloWorldApp",
3      filename: "remoteEntry.js",
4      exposes: {
5        "./HelloWorldButton":
6          "./src/components/hello-world-button/hello-world-button.js",
7        "./HelloWorldPage":
8          "./src/components/hello-world-page/hello-world-page.js",
9      },
10    }),
  • we do the same for production and dev configs
  • we do the same for our kiwi app
  • remember, we don't need remotes anymore, we need exposes
    • need to update the app.use path from '/static' in server.js
    • change the publicPath in both dev and production config
    • make sure the port is consistent across server and webpack configs

56 Micro frontends in action, part 1

  • now we create the dashboard app for our two micro frontends
  • create dashboard folder with src subfolder
  • we need:
    • a server.js
    • a package.json (npm init -y)
    • copy webpack configs from our apps and edit for our new dashboard app
  • note, the devServer entry in dev config is a little different
1 devServer: {
2    contentBase: path.resolve(__dirname, "./dist"),
3    index: "dashboard.html",
4    port: 9000,
5    historyApiFallback: {
6      index: "dashboard.html",
7    },
8  },
  • historyApiFallback tells webpack dev server to always return dashboard.html no matter what url is put into the browser
  • we don't need rules for images, scss (don't need for now)
  • we won't be using a template, so remove the .hbs rule our new dev config for dashboard (may contain typos):
1const path = require("path");
2const { CleanWebpackPlugin } = require("clean-webpack-plugin");
3const HtmlWebpackPlugin = require("html-webpack-plugin");
4const { ModuleFederationPlugin } = require("webpack").container;
5module.exports = {
6  entry: {
7    dashboard: "./src/dashboard.js",
8  },
9  output: {
10    filename: "[name].bundle.js",
11    path: path.resolve(__dirname, "./dist"),
12    publicPath: "http://localhost:9000",
13  },
14  mode: "development",
15  devServer: {
16    contentBase: path.resolve(__dirname, "./dist"),
17    index: "dashboard.html",
18    port: 9000,
19    historyApiFallback: {
20      index: "dashboard.html",
21    },
22  },
23  module: {
24    rules: [
25      {
26        test: /\.js$/,
27        exclude: /node_modules/,
28        use: {
29          loader: "babel-loader",
30          options: {
31            presets: ["@babel/env"],
32          },
33        },
34      },
35    ],
36  },
37  plugins: [
38    new CleanWebpackPlugin(),
39    new HtmlWebpackPlugin({
40      filename: "dashboard.html",
41      title: "Dashboard",
42    }),
43    new ModuleFederationPlugin({
44      name: "App",
45      remotes: {
46        HelloWorldApp: "HelloWorldApp@http://localhost:9001/remoteEntry.js",
47        KiwiApp: "KiwiApp@http://localhost:9005/remoteEntry.js",
48      },
49    }),
50  ],
51};
  • update production config similarly

57 micro frontends in action, part 2

  • remaining steps
    • import federated modules in our dashboard.js file and render them
  • had to overcome a middleware order bug I had in server.js. Find out more about the bug on Udemy
  • the dashboard.js file:
1const url = window.location.pathname;
2
3if (url === "/hello-world-page") {
4  import("HelloWorldApp/HelloWorldPage").then((HelloWorldPageModule) => {
5    const HelloWorldPage = HelloWorldPageModule.default;
6    const helloWorldPage = new HelloWorldPage();
7    helloWorldPage.render();
8  });
9} else if (url === "/kiwi-page") {
10  import("KiwiApp/KiwiPage").then((KiwiPageModule) => {
11    const KiwiPage = KiwiPageModule.default;
12    const kiwiPage = new KiwiPage();
13    kiwiPage.render();
14  });
15}
16
17console.log("dashboard");
  • we create a server.js filename
    • this is where my error crept In
    • I copied and pasted the server code and then updated it
    • however, in our dashboard server the order of our app.use and app.get is important since we are using '*' to match all routes for the get request
  • we install deps
  • most of the deps are dev only
    • express is a production dependency because we are using it to serve already generated files
  • we add our build and dev and start scripts to package.json
  • building our three apps and starting them, all is working as expected

58 Navigation bar component

  • since we're building a nav component, we want to design it with the end in mind
    • how will we invoke the navigation bar component?
    • how many arguments do we pass to it?
    • what should the arguments be?
  • one argument might be enough, a list of all of our pages
1const navigationItems = [
2  { url: "/kiwi-page", label: "Kiwi Page" },
3  { url: "/hello-world-page", label: "Hello World Page" },
4];
  • we will have a NavigationBar class and we'll pass our navigationItems as an argument:
1const navigationBar = new NavigationBar();
2navigationBar.render(navigationItems);
  • our navigation-bar.js file:
1import "./navigation-bar.scss";
2
3class NavigationBar {
4  render(navigationItems) {
5    const body = document.querySelector("body");
6    const liItems = navigationItems
7      .map((item) => {
8        return `<li><a href="${item.url}">${item.label}</a></li>`;
9      })
10      .join("");
11    const unorderedList = document.createElement("ul");
12    unorderedList.innerHTML = liItems;
13    body.appendChild(unorderedList);
14  }
15}
16
17export default NavigationBar;
  • our styles include a reset
1* {
2  box-sizing: border-box;
3  margin: 0;
4  padding: 0;
5}
  • much better to place your reset styles in a separate file
  • run the build and start and everything is working as intended
  • our finished app is a minimal example of a micro frontend architecture
    • our dashboard app acts as a container for multiple independent applications called micro frontends
    • the dashboard contains the routing logic that lets us switch between the independent apps
    • kiwi app and hello-world app are fully decoupled from the dashboard app and deployed on a different url
    • dashboard loads modules exposed by our micro apps and renders them on the page

59 Nested Module Federation, part 1

  • the module federation model we built up to lesson 58:

    basic module federation diagram with one container app and three remotes
  • here we have a single container app that consumes three remote applications
  • the dashboard app we produced only consumed two remotes
  • it is possible to have nested federated modules
  • for example
    • you might have a host app that consumes module one and module 2
    • module one might consume and be a container for module 3 and module 4
  • we'll now create this nested module federation app:

    nested module federation app diagram
  • we copy hello-world as app to create Image Caption application
  • update everything that needs to be updated and delete unused files
  • our folder structure is a little different
    • src folder
      • components folder
        • image-caption folder
          • image-caption.js
          • is the actual image caption component that is a paragraph
          • image-caption.scss
    • image-caption.js
    • this is for running the app in standalone mode
      • you might run it like this in development for testing
      • in production mode, you will likely run as a federated module which is part of the dashboard
  • once all files are updated, we npm install, run build, npm start and all is working

60 Nested module federation, part 2

  • we configure kiwi app so it can consume our ImageCaption app
1 remotes: {
2        ImageCaptionApp: "ImageCaptionApp@http://localhost:9003/remoteEntry.js",
3      },
  • note that kiw app has an exposes property and a remotes property
    • it therefore can act as a federated module for the dashboard and as a host application for image caption
  • we also need to change code in the component that is exposed from kiwi application
  • in our kiwi-page.js and kiwi.js (standalone)
1import("ImageCaptionApp/ImageCaption").then((ImageCaptionModule) => {
2  const ImageCaption = ImageCaptionModule.default;
3  const imageCaption = new ImageCaption();
4  imageCaption.render("kiwifruit is oval, a little bigger than a large egg.");
5});
  • note that we import ImageCaptionApp/ImageCaption
  • note we expose "./ImageCaption" and the name of our app is ImageCaptionApp
  • here viktor follows DRY principles and just reuses all the code from kiwi-page.js in kiwi.js (standalone)
  • kiwi.js looks like:
1import KiwiPage from "./components/kiwi-page/kiwi-page";
2
3const kiwiPage = new KiwiPage();
4kiwiPage.render();
  • the dashboard app consumes two federated modules and doesn't care if any of those consumed modules are themselves consuming federated modules
  • running all four apps, everything works as expected

61 getting the source code

  • just info on switching branches

62 Integration with jQuery

  • so, disregarding the usefulness of jQuery in 2023, let's finish this anyway!
  • used for DOM manipulation, animation, event handling
  • install jQuery via npm
  • import jQuery into heading.js
1import $ from "jquery";
  • the updated code using jQuery:
1import $ from "jquery";
2import "./heading.scss";
3
4class Heading {
5  render(pageName) {
6    const h1 = $("<h1>");
7    const body = $("body");
8    h1.text('Webpack is awesome. This is "' + pageName + '" page');
9    body.append(h1);
10  }
11}
12
13export default Heading;
  • that's it. Not really sure why this was included! Unless jQuery was popular when the course first came out

63 configuring ESLint

  • you can use ESLint even if you don't use webpack in your applications
  • what's a linter?
    • a linter refers to a tool that can analyze source code to identify programming errors, bugs, stylistic errors, and suspicious constructs
  • can warn you about
    • syntax errors
    • uses of undeclared variables
    • calls to deprecated functions
    • spacing and formating conventions
    • much more
  • linters for almost all programming languages
  • ESLint is open source
  • install ESLint
    • will install into node modules folder
  • add a script in package.json that will run ESLint for us
  • the script with no configuration file: js "lint": "eslint ."
  • create a config file in our root directory called '.eslintrc'
  • we can set up our rules in this file
  • there is a huge list of rules we can specify
  • there is a list of possible rules
  • some rules have check marks
    • these are widely used
    • they check common issues with your code
    • there is a shortcut to apply all of these check mark rules
  • inside .eslintrc:
1{
2  "extends": "eslint:recommended"
3}
  • this applies only the rules with check marks
  • still need to do more
  • by default ESLint only checks JavaScript against ECMAScript 5 standard
    • we are using ECMAScript 6
  • must configure more options
  • specifying more options:
1{
2  "extends": "eslint:recommended",
3  "parserOptions": {
4    "ecmaVersion": 6,
5    "sourceType": "module"
6  }
7}
  • ecmaVersion specifies the version of ECMAScript to check against
  • sourceType allows us to use ecmascript modules
    • otherwise ECMAScript would complain about import and export keywords
  • if we run ESLint now, we have a bunch of errors about document and require (amongst many other things)
  • require is defined when we use run build in our app since we are using a node environment
  • we need to let ESLint know that we are using a nodejs env
1 "env": {
2    "node": true,
3  }
  • no more complaints about node-specific keywords
  • our code will run in the browser, so document will be defined, need to let ESLint know this
1 "env": {
2    "node": true,
3    "browser": true
4  }
  • one more thing to do, we are using class properties in our application
    • they are not a part of official ECMAScript specification yet (or are they at this point?)
  • running lint now we get:
14:20 error Parsing error: Unexpected token =
  • this is our class property
  • to fix this, we use an additional parser for ESLint called babel-eslint
  • install: npm i babel-eslint --save-dev
  • add to config file (under 'extends'):
1"parser": "babel-eslint",
  • after running lint, getting errors about using require()
    • this seems related to babel-eslint being deprecated
  • install @babel/eslint-parser
1npm install eslint @babel/core @babel/eslint-parser --save-dev
  • update .eslintrc:
1 "parser": "@babel/eslint-parser",
  • it seems that there is a further error which relates to a config file for babel. However, if using babel with webpack, we can set an property in parserOptions, the full .eslintrc file:
1{
2  "extends": "eslint:recommended",
3  "parser": "@babel/eslint-parser",
4  "parserOptions": {
5    "requireConfigFile": false,
6    "ecmaVersion": 6,
7    "sourceType": "module"
8  },
9  "env": {
10    "node": true,
11    "browser": true
12  }
13}
  • note the requireConfigFile option is set to false since we are using babel with webpack
  • running lint, we have no errors
  • introducing an error, like an unused var and running lint again, we get a flagged error
  • all is working correctly
  • at this point, eslint is not flagging console.log, or console.debug, or console.warn
    • I am therefore not sure if it is working correctly
  • viktor addresses this in a reply to a question for the lecture
  • our eslintrc should look like:
1{
2  "extends": "eslint:recommended",
3  "parser": "@babel/eslint-parser",
4  "parserOptions": {
5    "requireConfigFile": false,
6    "sourceType": "module",
7    "ecmaVersion": "latest"
8  },
9  "env": {
10    "node": true,
11    "browser": true
12  }
13}
  • note, we are not getting no-console errors even though it appears they should be flagged
  • we can turn on or off individual rules like so:
1 "rules": {
2    "no-console": 1
3  }
  • note, this turns the no-console rule on
  • you can create a file that shows all active rules by running this in the console:
1npx eslint --print-config file.js > eslint-custom-config.json
  • up to now, we've run lint manually through the terminal
  • you can set up lint automatically in the editor
  • you can install in the editor
  • just installed ESLint in vscode, surprised I don't have it already?
    • although I have mostly been using create-react-app and so linting was probably being provided through that.

64 More hints about ESLint

  • this is a slide acknowledging that babel-eslint is deprecated
  • we also need to make a change in our package.json for this
1// before:
2    "lint": "eslint ."
3
4// after
5    "lint": "eslint src"

64 Summary

  • summary of what we learned