❤️ feel good a thumbs up yo ❤️. The original link

As Web development becomes more complex, we need tools to help us build modern websites. This is a complete online production example through a complex webpack4 configuration.

Building modern websites has become a custom application development, with websites expected to do more and have the functions of traditional applications rather than just being a promotional site.

As a process becomes more complex, we break it down into manageable components and automate it with tools such as building a car, drafting a bill [legal document], building a website.

Use the right tools to get the job done

Tools like WebPack have always been at the forefront of modern Web development, and because of that, they help us build complex things.

Webpack4 had some unexpected improvements, but the most interesting thing for me was how fast it got in terms of build speed, so I decided to go with it.

Hold on, because this is a long article full of information.

Using webpack

A little over A year ago, I published A Gulp Workflow for Frontend Development Automation that showed how to do the same thing using Gulp. However, during this time, I used more and more front-end frameworks like VUE-JS and GraphQL, such as the article Using VueJS + GraphQL to Make Practical Magic.

I find WebPack makes it easier for me to build various types of websites and applications, and it also allows me to use the most modern tool chains.

There are other options:

  • Laravel Mix is a webPack-based build tool layer, it’s very clean, you can get it up and running quickly, it does what you want 90% of the time, but the other 10% goes into WebPack anyway, and webPack 4 is not currently supported.

  • If you’re just using the VueJS front-end framework, vuE-CLI is a good choice, which is also webpack-based, works most of the time, and does some unexpected things for you. But again, you need WebPack when what it offers is no longer what you need, and I’m not just using VueJS.

  • Neutrino is also based on Webpack, and Neutrino: How I Learned to Stop Worrying and Love Webpack. The magic is that webPack can be configured like lego, but learning to use it costs just as much.

If you choose one of these tools (or any other), I won’t give you a hard time: they’re all webPack based.

It is good to understand how the development system middle layer works.

Ultimately, you just have to decide where you want to be on the front end of the technology pyramid.

At some point, I think it makes sense to understand how an important tool like WebPack works. Not long ago, I complained to Sean Larkin, one of the core webPack team, that WebPack was a “black box” and his answer was simple but pithily:

It’s only black if you haven’t opened It.

He’s right. It’s time to open the black box.

This article won’t teach you everything you need to know about WebPack, or even how to install it, but there are plenty of resources for you to choose what works best for you:

  • Webpack — The Confusing Parts — A brief overview of how Webpack works.

  • Webpack Documentation – I recommend reading the official Webpack documentation if you want to learn it well

  • Webpack Fundamentals — Webpack instructional videos

  • How to switch from Gulp to webpack

There are many more, but this article will instead configure and annotate a complete, complex working example using Webpack4. You can use the full example or some of its configuration items, but hopefully you’ll learn something from it. While I was learning about Webpack, I found that there were a lot of tutorial videos, a bunch of articles on how you would install it and add some basic configuration, but most of them didn’t have examples of webPack configuration for an actual online production environment, so I wrote this article.

WHAT WE GET OUT OF THE BOX

When I started learning webpack by opening the “black box,” I had a list of technologies I relied on that I wanted to make part of the build process. I also take the time to look around and see what else I can use in the process.

As discussed in the A Pretty Website Isn’t Enough Article, Website performance has always been A major concern of mine, so it’s not unusual to focus on performance when configuring WebPack.

So here’s what I want webPack to do for me, and what technologies I want to include in the build process:

  • Development/Production — In local Development, I build quickly with webpack-dev-server, and for Production builds (usually built in Docker containers via Buddy. Works), I want to optimize every point possible. Therefore, we distinguish between dev and PROd configuration and build.

  • Hot Module Replacement – When I change js, CSS, or a page, I want the page to refresh automatically, greatly improving development efficiency: you don’t have to hit the browser refresh button.

  • Dynamic Code Splitting – I didn’t want to manually define JS chunks in a configuration file, so I asked Webpack to help me solve this problem.

  • Lazy Loading — also known as asynchronous dynamic module Loading, which loads the required code resources when needed.

  • Modern & Legacy JS Bundles — I want to publish the ES2015 + JavaScript modules to browsers that support more than 75% of the world, with a patch pack (including all transcoding and Polyfills) for lower versions of browsers.

  • Cache Busting via manifest.json – allows us to Cache static resources while ensuring that they are recached automatically as they change.

  • Critical CSS – Improve the loading speed of the first page based on the article Implementing Critical CSS on your website.

  • Workbox Service Worker – We can use Google’s Workbox project to create a Service Worker for us and learn everything about our project. PWA, here we come!

  • PostCSS – I think of it as the “Babel of CSS”. Things like SASS and SCSS are built on it and it gives you access to upcoming CSS features.

  • Image Optimization – Images are still the main content of most web pages, so it is necessary to compress and optimize Image resources using automatic tools such as MozJPEG, OpTIPng, and SvGO.

  • Webp Creation – Chrome, Edge, and FireFox support. Webp files, which are smaller than JPEG and save resources.

  • VueJS – VueJS is the front-end framework I’m using this time and I want to be able to use a single file. vue component as part of the development process.

  • Tailwind CSS – Tailwind is a utility-first CSS that I use to quickly prototype in local development and then produce with PurgeCss to reduce the size.

Wow, that seems like quite a list!

There are many things, such as JavaScript automation, CSS compression, and other standard configurations, to build the desired front-end system.

I also want it to be available to development teams who can use different tools to apply to their local development environment and make the configuration easy to maintain and reusable for other projects.

The importance of maintainability and reusability can’t be understated.

You may use a different front-end framework or stack than I do, but the rules are the same, so read on for the rest, no matter what stack you use!

PROJECT TREE & ORGANIZATION

To give you an idea of the overall architecture of the application, here is a simple project tree:

├ ─ ─ example. Env ├ ─ ─ package. The json ├ ─ ─ postcss. Config. Js ├ ─ ─ the SRC │ ├ ─ ─ CSS │ │ ├ ─ ─ app. The PCSS │ │ ├ ─ ─ components │ │ │ ├ ─ ─ Global. PCSS │ │ │ ├ ─ ─ typography. The PCSS │ │ │ └ ─ ─ webfonts. PCSS │ │ ├ ─ ─ pages │ │ │ └ ─ ─ homepage. The PCSS │ │ └ ─ ─ vendor. The PCSS │ ├ ─ ─ fonts │ ├ ─ ─ img │ │ └ ─ ─ the favicon - SRC. PNG │ ├ ─ ─ js │ │ ├ ─ ─ app. Js │ │ └ ─ ─ workbox - catch - handler. Js │ └ ─ ─ vue │ └ ─ ─ Vue Flag School ── tailwind.config.js Flag School ── webPack.mon.js Flag School ── webPack.dev.js Flag School ── Webpack. Settings. Js └ ─ ─ yarn. The lockCopy the code

The complete code can be viewed at annotated-webpack-4-config

Methods in the core configuration file include:

  • Env — webpack-dev-server is a development-specific setting that does not need to be checked in Git

  • Webpack.settings. js – a JSON-ish Settings file, the only file we need to edit between projects

  • Webpack.common.js – The same type of build is placed in the uniform Settings file

  • Webpack.dev.js – Sets up individual builds for local development

  • Webpack.prod.js – Sets up individual builds in the production environment

Here is a diagram of how the above configurations are put together:

The goal is that you only need to edit the gold rounded areas (.env&webpack.settings.js) between the projects.

Separating out the configuration files in this form makes it much easier to use, even if you end up modifying the various WebPack configuration files I originally provided, but keeping them this way will help you maintain them over time.

Don’t worry, we’ll go through each file in detail later.

ANNOTATED PACKAGE.JSON

Let’s start by modifying our package.json:

{
    "name": "example-project"."version": "1.0.0"."description": "Example Project brand website"."keywords": [
        "Example"."Keywords"]."homepage": "https://github.com/example-developer/example-project"."bugs": {
        "email": "[email protected]"."url": "https://github.com/example-developer/example-project/issues"
    },
    "license": "SEE LICENSE IN LICENSE.md"."author": {
        "name": "Example Developer"."email": "[email protected]"."url": "https://example-developer.com"
    },
    "browser": "/web/index.php"."repository": {
        "type": "git"."url": "git+https://github.com/example-developer/example-project.git"
    },
    "private": true.Copy the code

There’s nothing interesting here, just meta information about our site, as described in the package.json specification.

"scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js"."build": "webpack --config webpack.prod.js --progress --hide-modules"
},
Copy the code

The above script represents the two main build steps we provided for the project:

  • Dev – Once the configuration is started, it uses webpack-dev-Server for hot module replacement (HMR), in-memory compilation, and other details, as long as we modify the project code.

  • Build – When we do a production deployment, it does all the fancy and time-consuming things like Critical CSS, JavaScript compression, etc.

You only need to perform the following operations on the command line: If yarn is used, enter yarn dev or YARN Build. If NPM is used, type NPM run dev or NPM run build. These are the only two commands you need to use.

Note that not only can we configure with –config, we can also pass in a separate configuration file. This allows us to split the Webpack configuration into separate logical files because we will be doing many different things for a development environment build than for a production environment build.

Next up is our Browserslist configuration:

"browserslist": {
        "production": [
            "1%" >."last 2 versions"."Firefox ESR"]."legacyBrowsers": [
            "1%" >."last 2 versions"."Firefox ESR"]."modernBrowsers": [
            "last 2 Chrome versions"."not Chrome < 60"."last 2 Safari versions"."not Safari < 10.1"."last 2 iOS versions"."not iOS < 10.3"."last 2 Firefox versions"."not Firefox < 54"."last 2 Edge versions"."not Edge < 15"]},Copy the code

This is a list of specific browsers based on human-readable configurations, PostCSS Autoprefixer is used by default in the production setting, We build legacyBrowsers and modernBrowsers on Babel to handle building traditional and modern js packages, described in more detail later.

Next comes devDependencies, which is all the NPM packages needed to build the system:

"devDependencies": {
        "@babel/core": "^ 7.1.0"."@babel/plugin-syntax-dynamic-import": "^ 7.0.0." "."@babel/plugin-transform-runtime": "^ 7.1.0"."@babel/preset-env": "^ 7.1.0"."@babel/register": "^ 7.0.0." "."@babel/runtime": "^ 7.0.0." "."autoprefixer": "^ 9.1.5." "."babel-loader": "^ 8.0.2." "."clean-webpack-plugin": "^ 0.1.19"."copy-webpack-plugin": "^ 4.5.2." "."create-symlink-webpack-plugin": "^ 1.0.0"."critical": "^" 1.3.4."critical-css-webpack-plugin": "^ 0.2.0." "."css-loader": "^ 1.0.0"."cssnano": "^ 4.1.0." "."dotenv": "^ 6.1.0"."file-loader": "^ 2.0.0." "."git-rev-sync": "^ 1.12.0"."glob-all": "^ 3.1.0"."html-webpack-plugin": "^ 3.2.0"."ignore-loader": "^ 0.1.2." "."imagemin": "^ 6.0.0"."imagemin-gifsicle": "^ 5.2.0." "."imagemin-mozjpeg": "^ 7.0.0." "."imagemin-optipng": "^ 5.2.1." "."imagemin-svgo": "^ 7.0.0." "."imagemin-webp": "^ 4.1.0." "."imagemin-webp-webpack-plugin": "^ 1.0.2"."img-loader": "^ 3.0.1." "."mini-css-extract-plugin": "^ 0.4.3." "."moment": "^ 2.22.2"."optimize-css-assets-webpack-plugin": "^ 5.0.1." "."postcss": "^ 7.0.2"."postcss-extend": "^ 1.0.5." "."postcss-hexrgba": "^" 1.0.1."postcss-import": "^ 12.0.0"."postcss-loader": "^ 3.0.0"."postcss-nested": "^ 4.1.0." "."postcss-nested-ancestors": "^ 2.0.0." "."postcss-simple-vars": "^ 5.0.1." "."purgecss-webpack-plugin": "^ 1.3.0"."purgecss-whitelister": "^ 2.2.0." "."resolve-url-loader": "^ 3.0.0"."sane": "^ 4.0.1." "."save-remote-file-webpack-plugin": "^ 1.0.0"."style-loader": "^ 0.23.0"."symlink-webpack-plugin": "^ 0.0.4"."terser-webpack-plugin": "^ 1.1.0." "."vue-loader": "^ 15.4.2." "."vue-style-loader": "^ 4.1.2." "."vue-template-compiler": "^ 2.5.17"."webapp-webpack-plugin": "https://github.com/brunocodutra/webapp-webpack-plugin.git"."webpack": "^ 4.19.1"."webpack-bundle-analyzer": "^ 3.0.2." "."webpack-cli": "^ 3.1.1." "."webpack-dashboard": "^ 2.0.0." "."webpack-dev-server": "^ 3.1.9." "."webpack-manifest-plugin": "^ 2.0.4"."webpack-merge": "^ 4.1.4." "."webpack-notifier": "^ 1.6.0." "."workbox-webpack-plugin": "^ 3.6.2." "
    },
Copy the code

Yes, there are a lot of NPM packages that we rely on, but our build process does require them.

Finally, the use of dependencies:

"dependencies": {
        "@babel/polyfill": "^ 7.0.0." "."axios": "^ 0.18.0." "."tailwindcss": "^ 0.6.6." "."vue": "^ 2.5.17"."vue-confetti": "^ 0.4.2"}}Copy the code

Obviously, for a real site or application, there will be more NPM packages in Dependencies, but for now we’ll focus on the build process.

ANNOTATED WEBPACK.SETTINGS.JS

I also use A similar approach I discussed in A Better Package. json for the Frontend Article to block the transition from inter-project configuration to A separate webpack.settings.js, leaving the Webpack configuration itself unchanged.

The key concept is that the only file we need to edit from project to project is the webpack.settings.js. [The key concept is that the only file we need to edit between projects is webpack.settings.js]

Since most projects have very similar things to do, we can create a WebPack configuration that works for each project, and we just need to change the data it operates on.

Thus, there is a separation of concerns between what is in our webpack.settings.js configuration file (the project-to-project data) and what is in the WebPack configuration (how this data is manipulated to produce the final result).

// webpack.settings.js - webpack settings config

// node modules
require('dotenv').config();

// Webpack settings exports
// noinspection WebpackConfigHighlighting
module.exports = {
    name: "Example Project",
    copyright: "Example Company, Inc.",
    paths: {
        src: {
            base: "./src/",
            css: "./src/css/",
            js: "./src/js/"
        },
        dist: {
            base: "./web/dist/",
            clean: [
                "./img"."./criticalcss"."./css"."./js"
            ]
        },
        templates: "./templates/"
    },
    urls: {
        live: "https://example.com/".local: "http://example.test/",
        critical: "http://example.test/",
        publicPath: "/dist/"
    },
    vars: {
        cssName: "styles"
    },
    entries: {
        "app": "app.js"
    },
    copyWebpackConfig: [
        {
            from: "./src/js/workbox-catch-handler.js",
            to: "js/[name].[ext]"
        }
    ],
    criticalCssConfig: {
        base: "./web/dist/criticalcss/",
        suffix: "_critical.min.css",
        criticalHeight: 1200,
        criticalWidth: 1200,
        ampPrefix: "amp_",
        ampCriticalHeight: 19200,
        ampCriticalWidth: 600,
        pages: [
            {
                url: "",
                template: "index"
            }
        ]
    },
    devServerConfig: {
        public: () => process.env.DEVSERVER_PUBLIC || "http://localhost:8080",
        host: () => process.env.DEVSERVER_HOST || "localhost",
        poll: () => process.env.DEVSERVER_POLL || false,
        port: () => process.env.DEVSERVER_PORT || 8080,
        https: () => process.env.DEVSERVER_HTTPS || false,
    },
    manifestConfig: {
        basePath: ""
    },
    purgeCssConfig: {
        paths: [
            "./templates/**/*.{twig,html}"."./src/vue/**/*.{vue,html}"
        ],
        whitelist: [
            "./src/css/components/**/*.{css,pcss}"
        ],
        whitelistPatterns: [],
        extensions: [
            "html"."js"."twig"."vue"
        ]
    },
    saveRemoteFileConfig: [
        {
            url: "https://www.google-analytics.com/analytics.js",
            filepath: "js/analytics.js"
        }
    ],
    createSymlinkConfig: [
        {
            origin: "img/favicons/favicon.ico",
            symlink: ".. /favicon.ico"
        }
    ],
    webappConfig: {
        logo: "./src/img/favicon-src.png",
        prefix: "img/favicons/"
    },
    workboxConfig: {
        swDest: ".. /sw.js",
        precacheManifestFilename: "js/precache-manifest.[manifestHash].js",
        importScripts: [
            "/dist/workbox-catch-handler.js"
        ],
        exclude: [
            /\.(png|jpe?g|gif|svg|webp)$/i,
            /\.map$/,
            /^manifest.*\\.js(?:on)?$/,
        ],
        globDirectory: "./web/",
        globPatterns: [
            "offline.html"."offline.svg"
        ],
        offlineGoogleAnalytics: true,
        runtimeCaching: [
            {
                urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
                handler: "cacheFirst",
                options: {
                    cacheName: "images",
                    expiration: {
                        maxEntries: 20
                    }
                }
            }
        ]
    }
};
Copy the code

We’ll cover all of this in the WebPack configuration section, but the important thing to note here is that we’ve taken the project-to-project changes and separated them from our WebPack configuration file and added them to a separate webpack.settings.js file.

This means that we can define the different parts of each project in the webpack.settings.js configuration file without having to mix with the webPack configuration itself. Even though the webpack.settings.js file is a JS file, I tried to keep it jSON-ish, so we just changed the simple Settings in it, and I didn’t have the flexibility to use JSON as a file format, and also allowed to add comments.

COMMON CONVENTIONS FOR WEBPACK CONFIGS

I have adopted some conventions for all webpack configuration files (webpack.common.js, webpack.dev.js, and webpack.prod.js) to make them look more consistent.

Each profile has two built-in configurations:

  • LegacyConfig — Configuration for older ES5 builds

  • ModernConfig – configuration for building modern ES2015+ versions

We did this because we had separate configurations to create compatible older versions with modern builds, making them logically independent. Webpack.common.js also has a baseConfig to keep the organization pure.

Think of it as object-oriented programming, where various configuration items inherit, with baseConfig as the root object.

Another convention used to keep the configuration clean and readable is to configure the Configure () function for the various WebPack plug-ins and any other Webpack fragments that need to be configured, rather than mixing them all together.

This is done because some data in webpack.settings.js needs to be transformed before using Webpack, and because of past/modern builds, we need to return different configurations depending on the build type.

It also makes the configuration file more readable.

As a general Webpack concept, remember that WebPack itself only knows how to load JavaScript and JSON. To load other things, you need to use the corresponding loader, and we’ll use many different loaders in our WebPack configuration.

ANNOTATED WEBPACK.COMMON.JS

Now let’s take a look at the webpack.common.js configuration file, which contains all the configurations shared between the dev and Prod build types.

// webpack.common.js - common webpack config
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';

// node modules
const path = require('path');
const merge = require('webpack-merge');

// webpack plugins
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const WebpackNotifierPlugin = require('webpack-notifier');

// config files
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
Copy the code

In the beginning, we introduced the Node package we needed, and the WebPack plug-in we needed to use. Then we import webpack.settings.js as Settings so we can access the Settings there, and import package.json as PKG to access it.

CONFIGURATION FUNCTIONS

Here is the setting for configureBabelLoader() :

// Configure Babel loader
const configureBabelLoader = (browserList) => {
   return {
       test: /\.js$/,
       exclude: /node_modules/,
       use: {
           loader: 'babel-loader',
           options: {
               presets: [
                   [
                       '@babel/preset-env', {
                       modules: false,
                       useBuiltIns: 'entry',
                       targets: {
                           browsers: browserList,
                       },
                   }
                   ],
               ],
               plugins: [
                   '@babel/plugin-syntax-dynamic-import'["@babel/plugin-transform-runtime", {
                       "regenerator": true}]],},},}; };Copy the code

The configureBabelLoader() function configureBabelLoader() configures babel-loader to handle the loading of all JS files and uses @babel/preset-env instead of the.babelrc file so we can keep all of this in the Webpack configuration file.

Babel can compile modern ES2015+ (and many other languages such as TypeScript or CoffeeScript) into browser-specific or standard JavaScript. We pass browserList as an argument so we can build modern ES2015+ modules for older browsers and with polyfills compatible with older ES5.

In our HTML, we only do things like this:

<! -- Browsers with ES module support load this file. --> <scripttype="module" src="main.js"></script> <! -- Older browsers load this file (and module-supporting --> <! -- browsers know *not* to load this file). --> <script nomodule src="main-legacy.js"></script>
Copy the code

Old browsers ignore the type=”module” script and get main-legacy.js. New browsers load main.js and ignore nomodule. It looks great. Just in case you think this approach is extreme, Vue-CLI uses this strategy in version 3.

The @babel /plugin-syntax-dynamic-import plug-in can be dynamically imported even before web browsers implement ECMAScripr dynamic imports, which allows us to load our JavaScript modules asynchronously and dynamically as needed.

So what are we talking about? That means we can do things like:

// App main
const main = async () => {
   // Async load the vue module
   const Vue = await import(/* webpackChunkName: "vue"* /'vue');
   // Create our vue instance
   const vm = new Vue.default({
       el: "#app",
       components: {
           'confetti': () => import(/* webpackChunkName: "confetti"* /'.. /vue/Confetti.vue'),}}); }; // Execute asyncfunction
main().then( (value) => {
});
Copy the code

There are two things:

/* webpackChunkName: “vue” */ we tell WebPack that we want this dynamic code split to be named.

2. Since we use import() in the asynchronous function (” main “), the function waits for the result of the dynamically loaded JavaScript import while the rest of the code continues on its way.

We have effectively told WebPack that we want our block to be split by code, not by configuration, and that the JavaScript block can be loaded asynchronously as needed through the built-in magic of @babel/plugin-syntax-dynamic-import.

Note that we did the same thing with the.vue single-file component, which is fine.

Instead of using await, we can also execute our code after the import()Promise returns:

// Async load the vue module
import(/* webpackChunkName: "vue"* /'vue').then(Vue => {
   // Vue has loaded, do something with it
   // Create our vue instance
   const vm = new Vue.default({
       el: "#app",
       components: {
           'confetti': () => import(/* webpackChunkName: "confetti"* /'.. /vue/Confetti.vue'),}}); });Copy the code

Here we use Promise instead of await, so we know that the dynamic import has been successful and we can happily use Vue.

If you are careful enough, you can see that we are effectively solving JavaScript dependencies with Promises. Awesome!

We can even do fun things like load JavaScript fast after the user clicks on something, scrolls to a location, or satisfies other criteria.

See more about Module Methods import().

If you are interested in learning more about Babel, check out the article Working with Babel 7 and Webpack.

Next we have configureEntries() :

// Configure Entries
const configureEntries = () => {
   let entries = {};
   for (const [key, value] of Object.entries(settings.entries)) {
       entries[key] = path.resolve(__dirname, settings.paths.src.js + value);
   }

   return entries;
};
Copy the code

Here we get the Webpack entry from webpack.settings.js via swttings.entries. For a single page application (SPA), there is only one entry. For more traditional sites, you might have several entries (maybe one entry per template page).

Either way, since we’ve defined entry points in webpack.settings.js, it’s easy to configure them in a file. Entry points are really just a

Since we are using dynamic import modules, we usually only have a tag on the page; The rest of the JavaScript is dynamically loaded as needed.

Next we have the configureFontLoader() function:

// Configure Font loader
const configureFontLoader = () => {
   return {
       test: /\.(ttf|eot|woff2?) $/i, use: [ { loader:'file-loader',
               options: {
                   name: 'fonts/[name].[ext]'}}}; };Copy the code

Dev and Prod build font loads are the same, so we’ll write it here, and for any native fonts we use, we can tell Webpack to load them in JavaScript:

import comicsans from '.. /fonts/ComicSans.woff2';
Copy the code

Next we have the configureManifest() function:

// Configure Manifest
const configureManifest = (fileName) => {
   return {
       fileName: fileName,
       basePath: settings.manifestConfig.basePath,
       map: (file) => {
           file.name = file.name.replace(/(\.[a-f0-9] {32}) (\.. *) $/,'$2');
           returnfile; }}; };Copy the code

This configates the Webpack-manifest-plugin for filene-based cache cleanup. Simply put, Webpack knows all the JavaScript, CSS, and other resources we need, so it can generate a pointer to a list of hashed resources, such as:

{
 "vendors~confetti~vue.js": "/dist/js/vendors~confetti~vue.03b9213ce186db5518ea.js"."vendors~confetti~vue.js.map": "/dist/js/vendors~confetti~vue.03b9213ce186db5518ea.js.map"."app.js": "/dist/js/app.30334b5124fa6e221464.js"."app.js.map": "/dist/js/app.30334b5124fa6e221464.js.map"."confetti.js": "/dist/js/confetti.1152197f8c58a1b40b34.js"."confetti.js.map": "/dist/js/confetti.1152197f8c58a1b40b34.js.map"."js/precache-manifest.js": "/dist/js/precache-manifest.f774c437974257fc8026ca1bc693655c.js".".. /sw.js": "/dist/.. /sw.js"
}
Copy the code

We pass in the filename because we create a modern monifest. Json and a manifest-legacy.json for compatibility, which have entry points for modern ES2015+ modules and compatibility with older ES5 modules, respectively. The key points in both JSON files are the same for resources generated for modern and older versions.

Next we have a fairly standard configureVueLoader() configuration:

// Configure Vue loader
const configureVueLoader = () => {
   return {
       test: /\.vue$/,
       loader: 'vue-loader'
   };
};
Copy the code

This configuration just makes it easy to parse Vue single file components, and WebPack takes care of extracting the appropriate HTML, CSS, and Javascript for you.

BASE CONFIG

BaseConfig will be merged with modernConfig and legacyConfig:

// The base webpack config
const baseConfig = {
   name: pkg.name,
   entry: configureEntries(),
   output: {
       path: path.resolve(__dirname, settings.paths.dist.base),
       publicPath: settings.urls.publicPath
   },
   resolve: {
       alias: {
           'vue$': 'vue/dist/vue.esm.js'
       }
   },
   module: {
       rules: [
           configureVueLoader(),
       ],
   },
   plugins: [
       new WebpackNotifierPlugin({title: 'Webpack', excludeWarnings: true, alwaysNotify: true}),
       new VueLoaderPlugin(),
   ]
};
Copy the code

All of the configurations here are very standard Webpack configurations, but notice that we point vue$to vue/dist/vue.esm.js so that we can get the ES2015 module version of vue.

We use the WebpackNotifierPlugin to tell us the status of the build in an intuitive way.

LEGACY CONFIG

The legacyConfig configuration is used to build compatible with older versions of ES5 using the appropriate Polyfill:

// Legacy webpack config
const legacyConfig = {
   module: {
       rules: [
           configureBabelLoader(Object.values(pkg.browserslist.legacyBrowsers)),
       ],
   },
   plugins: [
       new CopyWebpackPlugin(
           settings.copyWebpackConfig
       ),
       new ManifestPlugin(
           configureManifest('manifest-legacy.json'))]};Copy the code

Please note that we will PKG. Browserslist. LegacyBrowsers passed to configureBabelLoader (), will manifest – legacy. Json passed to configureManifest ().

We also joined the CopyWebpackPlugin plug-in in this configuration, we just need to copy Settings. CopyWebpackConfig defined in the file at a time.

MODERN CONFIG

ModernConfig is used to build modern ES2015 Javascript modules with no help from anything else:

// Modern webpack config
const modernConfig = {
   module: {
       rules: [
           configureBabelLoader(Object.values(pkg.browserslist.modernBrowsers)),
       ],
   },
   plugins: [
       new ManifestPlugin(
           configureManifest('manifest.json'))]};Copy the code

Please note that we will PKG. Browserslist. ModernBrowsers passed to configureBabelLoader (), will manifest. Json passed to configureManifest ().

MODULE.EXPORTS

Finally, Module.exports uses the webpack-merge plug-in to merge the previous configuration together and return the objects used by webpack.dev.js and webpack.prod.js.

// Common module exports
// noinspection WebpackConfigHighlighting
module.exports = {
   'legacyConfig': merge(
       legacyConfig,
       baseConfig,
   ),
   'modernConfig': merge(
       modernConfig,
       baseConfig,
   ),
};
Copy the code

ANNOTATED WEBPACK.DEV.JS

Now let’s take a look at the webpack.dev.js configuration file, which contains all the Settings we used to build our project, merged with the Settings in the webpack.common.js file to form a complete WebPack configuration.

// webpack.dev.js - developmental builds
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';

// node modules
const merge = require('webpack-merge');
const path = require('path');
const sane = require('sane');
const webpack = require('webpack');

// webpack plugins
const Dashboard = require('webpack-dashboard');
const DashboardPlugin = require('webpack-dashboard/plugin');
const dashboard = new Dashboard();

// config files
const common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
Copy the code

In the prologue, we reintroduce the Node packages we need to use, and the Webpack plug-in to use, then introduce webpack.settings.js as Settings so we can access the Settings there, and import package.json as PKG so we can access some of the Settings there.

We have also imported the webPack configuration commonly used by Webpack.common.js and will incorporate it into our development Settings.

CONFIGURATION FUNCTIONS

Here is the configuration for configureDevServer() :

// Configure the webpack-dev-server
const configureDevServer = (buildType) => {
   return{ public: settings.devServerConfig.public(), contentBase: path.resolve(__dirname, settings.paths.templates), host: settings.devServerConfig.host(), port: settings.devServerConfig.port(), https: !! parseInt(settings.devServerConfig.https()), quiet:true,
       hot: true,
       hotOnly: true,
       overlay: true,
       stats: 'errors-only', watchOptions: { poll: !! parseInt(settings.devServerConfig.poll()), ignored: /node_modules/, }, headers: {'Access-Control-Allow-Origin': The '*'
       },
       // Use sane to monitor all of the templates files and sub-directories
       before: (app, server) => {
           const watcher = sane(path.join(__dirname, settings.paths.templates), {
               glob: ['* * / *'], poll: !! parseInt(settings.devServerConfig.poll()), }); watcher.on('change'.function(filePath, root, stat) {
               console.log(' File modified:', filePath);
               server.sockWrite(server.sockets, "content-changed"); }); }}; };Copy the code

When we do a production build, Webpack binds all the various resources and saves them to the file system, in contrast, when we develop in a local project, we use development builds with webpack-dev-server:

  • Start the local Express Web server that serves our resources.

  • To speed things up, build our resources in memory instead of the file system.

  • Rebuild resources such as JavaScript, CSS, Vue components, etc., by using hot module update (HMR), when we modify these resources, we do not need to reload the interface.

  • The page is reloaded when the template is changed.

This is similar to the more complex Browsersync variant and greatly speeds up development.

The only difference here is that we use Sane to monitor files (our template in this case) that do not need to be run through WebPack and reload the page when the file is modified.

Note that the webpack-dev-server configuration again refers to the webpack.settings.js file, which is probably fine by default for most people, but I use Laravel Homestead for local development, As discussed in our article Local Development with Vagrant/Homestead, this means that I run all of my Development tools in the Homestead VM.

Thus, webpack.settings.js can read from an. Env file with a specific devServer configuration, rather than hardcoding the local development environment in my weboack.settings.js file (since it may vary from person to person) :

// .env file DEVSERVER settings
# webpack example settings for Homestead/Vagrant
DEVSERVER_PUBLIC="http://192.168.10.10:8080"
DEVSERVER_HOST="0.0.0.0"
DEVSERVER_POLL=1
DEVSERVER_PORT=8080
DEVSERVER_HTTPS=0
Copy the code

You can use different configurations, so you can change the Settings as needed in the.env file. The idea behind Dotenv is that we define an environment-specific configuration in the.env file and don’t check it into the Git repo. If the. Env file doesn’t exist, that’s fine, use the default:

// webpack.settings.js devServerConfig defaults
devServerConfig: {
    public: () => process.env.DEVSERVER_PUBLIC || "http://localhost:8080",
    host: () => process.env.DEVSERVER_HOST || "localhost",
    poll: () => process.env.DEVSERVER_POLL || false,
    port: () => process.env.DEVSERVER_PORT || 8080,
    https: () => process.env.DEVSERVER_HTTPS || false,},Copy the code

Next comes the configureImageLoader() configuration:

// webpack.dev.js configureImageLoader()
// Configure Image loader
const configureImageLoader = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            test: /\.(png|jpe? g|gif|svg|webp)$/i, use: [ { loader:'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'}}}; }if (buildType === MODERN_CONFIG) {
        return {
            test: /\.(png|jpe? g|gif|svg|webp)$/i, use: [ { loader:'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'}}}; }};Copy the code

Pass in the buildType parameter to return different results, depending on whether it’s an old or a new build. In this case, we returned the same configuration, but it could conceivably change.

It’s worth noting that this only applies to images included in our WebPack build; Many other images come from other places (CMS, asset management systems, etc.).

To let WebPack know there is an image, import it into your JavaScript file:

import Icon from './icon.png';
Copy the code

For more details on this, see the Load Images section of the Webpack documentation.

Next comes the configurePostcssLoader() configuration:

// Configure the Postcss loader
const configurePostcssLoader = (buildType) => {
    // Don't generate CSS for the legacy config in development if (buildType === LEGACY_CONFIG) { return { test: /\.(pcss|css)$/, loader: 'ignore-loader'}; } if (buildType === MODERN_CONFIG) { return { test: /\.(pcss|css)$/, use: [ { loader: 'style-loader', }, { loader: 'vue-style-loader', }, { loader: 'css-loader', options: { importLoaders: 2, sourceMap: true } }, { loader: 'resolve-url-loader' }, { loader: 'postcss-loader', options: { sourceMap: true } } ] }; }};Copy the code

We use PostCSS for all CSS, including Tailwind CSS. I think of PostCSS as the Babel of CSS, programming various advanced CSS functions into normal CSS that browsers can parse.

For webPack loaders, they are processed in the reverse order of listing:

  • Postcss-loader – loads and processes files as postCSS

  • Resolve-url-loader — Rewrite all urls () in CSS as relative paths

  • Css-loader — parse all of our CSs@import and URLS ()

  • Vue-style-loader – Will inject all CSS into a. Vue single file.

  • Style-loader – inject all CSS into the

We don’t need to extract all CSS files into the smallest file during local development, instead we just let style-loader inline it in our document.

Webpack-dev-server uses hot module replacement (HMR) for CSS, and every time we change the style, it rebuilds the CSS and automatically infuses it, magically (what).

We introduce it to tell WebPack to parse:

import styles from '.. /css/app.pcss';
Copy the code

This is discussed in detail in the Loading CSS section of the Webpack documentation.

We do this from the app.js entry point, which we think of as the PostCSS entry point. The app.pcss file @import all the CSS used in our project, which will be described in more detail later.

MODULE.EXPORTS

Finally, module.exports uses the webpack-merge package to merge common.legacyConfig from webpack.common.js with our development legacy compatibility configuration and common.modernConfig with the modern configuration of the development environment:

// Development module exports
module.exports = [
    merge(
        common.legacyConfig,
        {
            output: {
                filename: path.join('./js'.'[name]-legacy.[hash].js'),
                publicPath: settings.devServerConfig.public() + '/',
            },
            mode: 'development',
            devtool: 'inline-source-map',
            devServer: configureDevServer(LEGACY_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(LEGACY_CONFIG),
                    configureImageLoader(LEGACY_CONFIG),
                ],
            },
            plugins: [
                new webpack.HotModuleReplacementPlugin(),
            ],
        }
    ),
    merge(
        common.modernConfig,
        {
            output: {
                filename: path.join('./js'.'[name].[hash].js'),
                publicPath: settings.devServerConfig.public() + '/',
            },
            mode: 'development',
            devtool: 'inline-source-map',
            devServer: configureDevServer(MODERN_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(MODERN_CONFIG),
                    configureImageLoader(MODERN_CONFIG),
                ],
            },
            plugins: [
                new webpack.HotModuleReplacementPlugin(),
                new DashboardPlugin(dashboard.setData),
            ],
        }
    ),
];
Copy the code

By returning an array from module.exports, we tell WebPack that there are multiple compilations to perform: one for the old compatible build and one for the new build.

For the old build, we named the processed JavaScript [name]-legacy.[hash].js, while the new build is named [name].[hash].js.

Tell WebPack that this is a development environment build by setting mode to development.

If devtool is set to inline-source-map, we require CSS/JavsScript.map to be inline-map in the file, which will make the project larger but easier to debug.

Through webpack HotModuleReplacementPlugin plug-in, can support the webpack thermal module to replace (HMR).

The DashboardPlugin makes us feel like astronauts with a cool panel:

I find the DashboardPlugin plugin to develop a HUD more intuitive than the default Webpack display of progress.

At this point, we now have a good development environment configuration for our project. Check out the hot module replacement video for an example of this operation.

ANNOTATED WEBPACK.PROD.JS

Now let’s take a look at the webpack.prod.js configuration file, which contains all the configuration for the production build while we’re working on the project. It merges with the Settings in webpack.common.js to form a complete Webpack configuration.

// webpack.prod.js - production builds
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';

// node modules
const git = require('git-rev-sync');
const glob = require('glob-all');
const merge = require('webpack-merge');
const moment = require('moment');
const path = require('path');
const webpack = require('webpack');

// webpack plugins
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CreateSymlinkPlugin = require('create-symlink-webpack-plugin');
const CriticalCssPlugin = require('critical-css-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ImageminWebpWebpackPlugin = require('imagemin-webp-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const SaveRemoteFilePlugin = require('save-remote-file-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const WebappWebpackPlugin = require('webapp-webpack-plugin');
const WhitelisterPlugin = require('purgecss-whitelister');
const WorkboxPlugin = require('workbox-webpack-plugin');

// config files
const common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
Copy the code

We reimported the Node package we covered in the prologue and the Webpack plug-in we used, then imported webpack.settings.js as Settings and package.json as PKG for easy access to the configuration we needed.

We also imported the common WebPack configuration in Webpack.common.js, which we will merge with the development Settings.

TAILWIND EXTRACTOR

This class is a custom PurgeCSS extractor for Tailwind CSS, allowing special characters in class names.

// Custom PurgeCSS extractor for Tailwind that allows special characters in
// class names.
//
// https://github.com/FullHuman/purgecss#extractor
class TailwindExtractor {
    static extract(content) {
        returncontent.match(/[A-Za-z0-9-_:\/]+/g) || []; }}Copy the code

This is taken from the part of the Tailwind CSS document where you remove unused CSS with PurgeCSS. For more information on how this extractor works with PURgCSS, see below to make your CSS cleaner.

CONFIGURATION FUNCTIONS

This is the configureBanner() function:

// Configure file banner
const configureBanner = () => {
    return {
        banner: [
            '/ *! '.' * @project ' + settings.name,
            ' * @name ' + '[filebase]'.' * @author ' + pkg.author.name,
            ' * @build ' + moment().format('llll') + ' ET'.' * @release ' + git.long() + '[' + git.branch() + '] '.' * @copyright Copyright (c) ' + moment().format('YYYY') + ' ' + settings.copyright,
            The '*'.' */'.' '
        ].join('\n'),
        raw: true
    };
};
Copy the code

This simply adds a banner with the project name, filename, author, and Git information for each file we generate.

ConfigureBundleAnalyzer () follows:

// webpack.prod.js configureBundleAnalyzer()
// Configure Bundle Analyzer
const configureBundleAnalyzer = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            analyzerMode: 'static',
            reportFilename: 'report-legacy.html'}; }if (buildType === MODERN_CONFIG) {
        return {
            analyzerMode: 'static',
            reportFilename: 'report-modern.html'}; }};Copy the code

Use the WebpackBundleAnalyzer plug-in to generate a report for our new and old builds, as well as a separate interactive HTML page to see the exact content of the webPack package.

I found this plug-in useful in reducing the size of my final build pack, and knowing exactly what WebPack builds, I’ve made it part of my project’s production build process.

ConfigureCriticalCss () follows:

// webpack.prod.js configureCriticalCss()
// Configure Critical CSS
const configureCriticalCss = () => {
    return (settings.criticalCssConfig.pages.map((row) => {
            const criticalSrc = settings.urls.critical + row.url;
            const criticalDest = settings.criticalCssConfig.base + row.template + settings.criticalCssConfig.suffix;
            let criticalWidth = settings.criticalCssConfig.criticalWidth;
            let criticalHeight = settings.criticalCssConfig.criticalHeight;
            // Handle Google AMP templates
            if(row.template.indexOf(settings.criticalCssConfig.ampPrefix) ! == -1) { criticalWidth = settings.criticalCssConfig.ampCriticalWidth; criticalHeight = settings.criticalCssConfig.ampCriticalHeight; } console.log("source: " + criticalSrc + " dest: " + criticalDest);
            return new CriticalCssPlugin({
                base: '/',
                src: criticalSrc,
                dest: criticalDest,
                extract: false,
                inline: false,
                minify: true,
                width: criticalWidth,
                height: criticalHeight,
            })
        })
    );
};
Copy the code

Use CriticalCssPlugin plug-in through webpack. Settings. Settings in the js. CriticalCssConfig. The pages are partitioned, generated CriticalCSS for our website.

It is important to note that if the incoming page contains Settings in all position name. CriticalCssConfig. AmpPrefix, it will by passing in a very large height for the entire page (and not just the content above the fold) generated CriticalCSS.

CriticalCSS will not be covered in detail here, but for more information on it, see the Implementing CriticalCSS on Your Website article.

ConfigureCleanWebpack () follows:

// Configure Clean webpack
const configureCleanWebpack = () => {
    return {
        root: path.resolve(__dirname, settings.paths.dist.base),
        verbose: true,
        dry: false
    };
};
Copy the code

This simply removes the build directory in settings.paths.dist. Base from our webpack.settings.js using the CleanWebpackPlugin.

ConfigureHtml () follows:

// Configure Html webpack
const configureHtml = () => {
    return {
        templateContent: ' ',
        filename: 'webapp.html',
        inject: false}; };Copy the code

This will generate the HTML for our favicons using the HtmlWebpackPlugin and the WebappWebpackPlugin (see below) plug-in. Notice that we pass an empty string in templateContent so that the output is just the raw output of the WebappWebpackPlugin.

ConfigureImageLoader () follows:

// Configure Image loader
const configureImageLoader = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            test: /\.(png|jpe? g|gif|svg|webp)$/i, use: [ { loader:'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'}}}; }if (buildType === MODERN_CONFIG) {
        return {
            test: /\.(png|jpe? g|gif|svg|webp)$/i, use: [ { loader:'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'
                    }
                },
                {
                    loader: 'img-loader',
                    options: {
                        plugins: [
                            require('imagemin-gifsicle')({
                                interlaced: true,
                            }),
                            require('imagemin-mozjpeg')({
                                progressive: true,
                                arithmetic: false,
                            }),
                            require('imagemin-optipng')({
                                optimizationLevel: 5,
                            }),
                            require('imagemin-svgo')({
                                plugins: [
                                    {convertPathData: false},]}),]}}]}; }};Copy the code

We pass in the buildType parameter so that we can return different results depending on whether it is a new or old build. We processed the image through optimization and built the new version through IMg-Loader.

We only do this for the new build, because it doesn’t make sense to spend time processing and optimizing the images for the new and old versions (the images are the same for both).

Note that this only applies to images included in our Webpack build, many other image resources actually come from other sources (CMS, asset management systems, etc.).

To get WebPack to optimize the image, import it into JavaScript:

import Icon from './icon.png';
Copy the code

For more details on Loading Images, see the corresponding section of the Webpack documentation.

Next comes our configureOptimization() configuration:

// Configure optimization
const configureOptimization = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            splitChunks: {
                cacheGroups: {
                    default: false,
                    common: false,
                    styles: {
                        name: settings.vars.cssName,
                        test: /\.(pcss|css|vue)$/,
                        chunks: 'all',
                        enforce: true
                    }
                }
            },
            minimizer: [
                new TerserPlugin(
                    configureTerser()
                ),
                new OptimizeCSSAssetsPlugin({
                    cssProcessorOptions: {
                        map: {
                            inline: false,
                            annotation: true,
                        },
                        safe: true,
                        discardComments: true}}})]; }if (buildType === MODERN_CONFIG) {
        return{ minimizer: [ new TerserPlugin( configureTerser() ), ] }; }};Copy the code

This is an optimized configuration for the WebPack production environment, and for older builds (it doesn’t make sense to do this twice), we used the MiniCssExtractPlugin to extract the CSS used in the project into a single CSS file. If you’ve used WebPack before, you’ve probably used the ExtractTextPlugin to do this before, but you don’t need to do it now.

We also used the OptimizeCSSAssetsPlugin plug-in to optimize the generated CSS by removing duplicate rules and to compress the CSS through CSsnano.

Finally, we set Javascript Minimizer to TerserPlugin because of [UglifyJsPlugin] (github.com/webpack-con…) Minimal ES2015+JavaScript is no longer supported. Since we are generating a new version of ES2015 + Bundles, we need it.

ConfigurePostcssLoader () follows:

// Configure Postcss loader
const configurePostcssLoader = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            test: /\.(pcss|css)$/,
            use: [
                MiniCssExtractPlugin.loader,
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2,
                        sourceMap: true
                    }
                },
                {
                    loader: 'resolve-url-loader'
                },
                {
                    loader: 'postcss-loader',
                    options: {
                        sourceMap: true}}}; } // Don't generate CSS for the modern config in production if (buildType === MODERN_CONFIG) { return { test: /\.(pcss|css)$/, loader: 'ignore-loader'}; }};Copy the code

The configuration looks very similar to the development version configurePostcssLoader (), in addition to the final loader, we use MiniCssExtractPlugin. The loader will all CSS extracted to a file.

We only do this for older compatible builds, because it doesn’t make sense to do it for every build (CSS is the same). We built the new version using ignore-loader, so there is a loader for our.css and.pcss files, but nothing is done.

As mentioned earlier, we use PostCSS for all CSS, including Tailwind CSS, which I consider to be the Babel of CSS because it compiles various advanced CSS functions into normal CSS that your browser can parse.

Also, for webPack loaders, they are processed in the reverse order listed:

  • Postcss-loader – loads and processes files as postCSS

  • Resolve-url-loader — Rewrite all urls () in CSS as relative paths

  • Css-loader — parse all of our CSs@import and URLS ()

  • MiniCssExtractPlugin. Loader – will all CSS extracted to a file

Since this is a production environment to build, we use MiniCssExtractPlugin. Loader to extract all use CSS, and save the. CSS file. CSS is also minimized and optimized for production environments.

We tell Webpack by introducing a CSS file:

import styles from '.. /css/app.pcss';
Copy the code

This is described in detail in the Loading CSS of the Webpack documentation.

We do this from the app.js entry point, which we think of as the postCSS entry point. The app.pcss file @import all the CSS we use for our project, which we’ll cover in more detail later.

ConfigurePurgeCss () follows:

// Configure PurgeCSS
const configurePurgeCss = () => {
    let paths = [];
    // Configure whitelist paths
    for (const [key, value] of Object.entries(settings.purgeCssConfig.paths)) {
        paths.push(path.join(__dirname, value));
    }

    return {
        paths: glob.sync(paths),
        whitelist: WhitelisterPlugin(settings.purgeCssConfig.whitelist),
        whitelistPatterns: settings.purgeCssConfig.whitelistPatterns,
        extractors: [
            {
                extractor: TailwindExtractor,
                extensions: settings.purgeCssConfig.extensions
            }
        ]
    };
};
Copy the code

Tailwind CSS is an excellent utility-first CSS framework that allows for rapid prototyping because in native development, there is very little need to actually write any CSS. Instead, you just need to use the provided utility CSS classes.

The downside is that the generated CSS can be a bit large, which is when you need PurgeCSS, which will parse all HTML/template/Vue/ any files and delete any unused CSS.

The space savings can be huge, and Tailwind CSS and PurgeCSS are a match made in heaven. We discuss this in depth at Tailwind CSS Utility – First CSS with Adam Wathan.

It traverses Settings. PurgeCssConfig. All paths globs of paths, for keep the CSS rules, any not found CSS rules will be deleted from the we generate CSS build.

We also use the WhitelisterPlugin, which makes it easy to whitelist entire files or globally when we know we don’t want some CSS to be stripped. With our Settings. PurgeCssConfig. Whitelist matching the CSS rules in all of the files are listed in the white list, and will never be deleted from the generated build.

Next is configureTerser() :

// Configure terser
const configureTerser = () => {
    return {
        cache: true,
        parallel: true.sourceMap: true
    };
};
Copy the code

This is just configuring the [TerserPlugin] (github.com/webpack-con…) Some Settings are used to minimize our old and new JavaScript code.

ConfigureWebApp () follows:

// Configure Webapp webpack
const configureWebapp = () => {
    return {
        logo: settings.webappConfig.logo,
        prefix: settings.webappConfig.prefix,
        cache: false,
        inject: 'force',
        favicons: {
            appName: pkg.name,
            appDescription: pkg.description,
            developerName: pkg.author.name,
            developerURL: pkg.author.url,
            path: settings.paths.dist.base,
        }
    };
};
Copy the code

Here we use WebAppWebPackePulin to generate all of our website Favicons in countless formats, as well as our WebApp Manifest.json and other PWA details.

It is used in conjunction with the HtmlWebpackPlugin and can also output a webapp.html file which contains all generated favicons and links to related files to be included in of our HTML page.

ConfigureWorkbox () follows:

// Configure Workbox service worker
const configureWorkbox = () => {
    let config = settings.workboxConfig;

    return config;
};
Copy the code

We use Google’s WorkboxWebpackPlugin to generate a Service Worker for a website. It is beyond the scope of this article to explain what a Service Worker is, but check out Going Offline: Service Workers with Jeremy Keith blog as an introduction.

The configuration data is all from the settings.workboxConfig object in webpack.settings.js. In addition to pre-caching all the resources in the new build minifest. Json, we also include a workbox-catch-handler.js to configure it to use the fallback response catch-all route.

// fallback URLs
const FALLBACK_HTML_URL = '/offline.html';
const FALLBACK_IMAGE_URL = '/offline.svg';

// This "catch" handler is triggered when any of the other routes fail to
// generate a response.
// https://developers.google.com/web/tools/workbox/guides/advanced-recipes#provide_a_fallback_response_to_a_route
workbox.routing.setCatchHandler(({event, request, url}) => {
    // Use event, request, and url to figure out how to respond.
    // One approach would be to use request.destination, see
    // https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c
    switch (request.destination) {
        case 'document':
            return caches.match(FALLBACK_HTML_URL);
            break;

        case 'image':
            return caches.match(FALLBACK_IMAGE_URL);
            break;

        default:
            // If we don't have a fallback, just return an error response. return Response.error(); }}); // Use a stale-while-revalidate strategy for all other requests. workbox.routing.setDefaultHandler( workbox.strategies.staleWhileRevalidate() );Copy the code

MODULE.EXPORTS

Finally, module.export uses webpack-merge to merge common.legacyConfig from webpack.mons.js with the old configuration of our production environment. And merge common.modernConfig with our new production environment configuration:

// Production module exports
module.exports = [
    merge(
        common.legacyConfig,
        {
            output: {
                filename: path.join('./js'.'[name]-legacy.[chunkhash].js'),
            },
            mode: 'production',
            devtool: 'source-map',
            optimization: configureOptimization(LEGACY_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(LEGACY_CONFIG),
                    configureImageLoader(LEGACY_CONFIG),
                ],
            },
            plugins: [
                new CleanWebpackPlugin(settings.paths.dist.clean,
                    configureCleanWebpack()
                ),
                new MiniCssExtractPlugin({
                    path: path.resolve(__dirname, settings.paths.dist.base),
                    filename: path.join('./css'.'[name].[chunkhash].css'),
                }),
                new PurgecssPlugin(
                    configurePurgeCss()
                ),
                new webpack.BannerPlugin(
                    configureBanner()
                ),
                new HtmlWebpackPlugin(
                    configureHtml()
                ),
                new WebappWebpackPlugin(
                    configureWebapp()
                ),
                new CreateSymlinkPlugin(
                    settings.createSymlinkConfig,
                    true
                ),
                new SaveRemoteFilePlugin(
                    settings.saveRemoteFileConfig
                ),
                new BundleAnalyzerPlugin(
                    configureBundleAnalyzer(LEGACY_CONFIG),
                ),
            ].concat(
                configureCriticalCss()
            )
        }
    ),
    merge(
        common.modernConfig,
        {
            output: {
                filename: path.join('./js'.'[name].[chunkhash].js'),
            },
            mode: 'production',
            devtool: 'source-map',
            optimization: configureOptimization(MODERN_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(MODERN_CONFIG),
                    configureImageLoader(MODERN_CONFIG),
                ],
            },
            plugins: [
                new webpack.optimize.ModuleConcatenationPlugin(),
                new webpack.BannerPlugin(
                    configureBanner()
                ),
                new ImageminWebpWebpackPlugin(),
                new WorkboxPlugin.GenerateSW(
                    configureWorkbox()
                ),
                new BundleAnalyzerPlugin(
                    configureBundleAnalyzer(MODERN_CONFIG),
                ),
            ]
        }
    ),
];
Copy the code

By returning an array in our module.exports, we tell WebPack that there are multiple compilations to be done: one for the old compatible build and one for the new build.

Note that for the old compatible build, we output the processed JavaScript as [name]-legacy.[hash].js, while the new build outputs it as [name].[hash].js.

By setting mode to Production, we tell WebPack that this is a production build, which will enable a number of Settings applicable to production.

By setting devtool to source-map, we require CSS/JavaScript to generate separate.map files, which makes it easier to debug the site in real-time production environments without having to add resource file sizes.

There are a few WebPack plugins used here that we haven’t covered yet:

  • CreateSymlinkPlugin – This is a plug-in I created that allows symbolic links to be created during the build process to link generated Favicon.ico symbols to /favicon.ico, as many Web browsers look in the Web root directory.

  • SaveRemoteFilePlugin – Used to download remote files and output them as part of the WebPack build process. I use it to download and provide Google analytics.

  • ImageminWebpWebpackPlugin – will this plug-in for project import all the JPEG, and PNG file creation. Webp variants.

Until now, we have provided a good production environment build for the project.

TAILWIND CSS & POSTCSS CONFIG

In order for WebPack to properly build Tailwind CSS and other CSS, we need to do a few things. Thanks to my partner Jonathan Melville for his work on this construction, first we need a postcss.config.js file:

module.exports = {
    plugins: [
        require('postcss-import'),
        require('postcss-extend'),
        require('postcss-simple-vars'),
        require('postcss-nested-ancestors'),
        require('postcss-nested'),
        require('postcss-hexrgba'),
        require('autoprefixer'),
        require('tailwindcss') ('./tailwind.config.js')]};Copy the code

This can be stored in the project root directory, and PostCSS will automatically look for it during the build process and apply the PostCSS plug-in we specify. Note that this is where we introduced the tailwind.config.js file so that it becomes part of the build process.

Finally, our CSS entry point app.pcss looks like this:

/**
 * app.css
 *
 * The entry point for the css.
 *
 */

/**
 * This injects Tailwind's base styles, which is a combination of * Normalize.css and some additional base styles. * * You can see the styles here: * https://github.com/tailwindcss/tailwindcss/blob/master/css/preflight.css */ @import "tailwindcss/preflight"; /** * This injects any component classes registered by plugins. * */ @import 'tailwindcss/components'; /** * Here we add custom component classes; stuff we want loaded * *before* the utilities so that the utilities can still * override them. * */ @import './components/global.pcss';
@import './components/typography.pcss';
@import './components/webfonts.pcss'; /** * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 *
 */
@import 'tailwindcss/utilities';

/**
 * Include styles for individual pages
 *
 */
@import './pages/homepage.pcss';

/**
 * Include vendor css.
 *
 */
 @import 'vendor.pcss';
Copy the code

Obviously, customize it to include any components/interfaces for custom CSS.

POST-BUILD PROJECT TREE

This is the structure of our project after construction:

├ ─ ─ example. Env ├ ─ ─ package. The json ├ ─ ─ postcss. Config. Js ├ ─ ─ the SRC │ ├ ─ ─ CSS │ │ ├ ─ ─ app. The PCSS │ │ ├ ─ ─ components │ │ │ ├ ─ ─ Global. PCSS │ │ │ ├ ─ ─ typography. The PCSS │ │ │ └ ─ ─ webfonts. PCSS │ │ ├ ─ ─ pages │ │ │ └ ─ ─ homepage. The PCSS │ │ └ ─ ─ vendor. The PCSS │ ├ ─ ─ fonts │ ├ ─ ─ img │ │ └ ─ ─ the favicon - SRC. PNG │ ├ ─ ─ js │ │ ├ ─ ─ app. Js │ │ └ ─ ─ workbox - catch - handler. Js │ └ ─ ─ vue │ └ ─ ─ Confetti. Vue ├ ─ ─ tailwind. Config. Js ├ ─ ─ templates ├ ─ ─ web │ ├ ─ ─ dist │ │ ├ ─ ─ criticalcss │ │ │ └ ─ ─ Index_critical. Min. CSS │ │ ├ ─ ─ CSS │ │ │ ├ ─ ─ styles. D833997e3e3f91af64e7. CSS │ │ │ └ ─ ─ Styles. D833997e3e3f91af64e7. CSS. The map │ │ ├ ─ ─ img │ │ │ └ ─ ─ favicons │ │ │ ├ ─ ─ android - chrome - 144 x144. PNG │ │ │ ├ ─ ─ │ │ ├─ Heavy Metal Guitar School ─ Heavy metal Metal Guitar School ─ Heavy metal metal Metal Metal Metal Metal Metal Metal Metal Metal Metal Metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal │ │ ├─ Heavy Metal Guitar School ─ Heavy metal Metal Guitar School ─ Heavy metal metal metal Metal Metal Metal Metal Metal Metal Metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal metal │ │ ├─ ├─ Apple - Touch - Icon -114x114. PNG │ │ ├─ Apple - Touch - Icon -114x114 Apple - touch - icon - 120 x120 measures how. PNG │ │ │ ├ ─ ─ apple - touch - icon - 144 x144. PNG │ │ │ ├ ─ ─ apple - touch - icon - 152 x152. PNG │ │ │ ├ ─ ─ Apple - touch - icon - 167 x167. PNG │ │ │ ├ ─ ─ apple - touch - icon - 180 x180. PNG │ │ │ ├ ─ ─ apple - touch - icon - 57 x57. PNG │ │ │ ├ ─ ─ Apple - touch - icon - 60 x60. PNG │ │ │ ├ ─ ─ apple - touch - icon - 72 x72. PNG │ │ │ ├ ─ ─ apple - touch - icon - 76 x76. PNG │ │ │ ├ ─ ─ Apple - touch - icon. PNG │ │ │ ├ ─ ─ apple - touch - icon - precomposed. PNG │ │ │ ├ ─ ─ apple - touch - startup - image - 1182 x2208. PNG │ │ │ ├ ─ ─ apple - touch - startup - image - 1242 x2148. PNG │ │ │ ├ ─ ─ apple - touch - startup - image - 1496 x2048. PNG │ │ │ ├ ─ ─ Apple - touch - startup - image - 1536 x2008. PNG │ │ │ ├ ─ ─ apple - touch - startup - image - 320 x460. PNG │ │ │ ├ ─ ─ │ │ ├─ Apple - Startup-Image-640x1096. PNG │ │ ├─ Apple - Startup-Image-640x1096 Apple - touch - startup - image - 748 x1024. PNG │ │ │ ├ ─ ─ apple - touch - startup - image - 750 x1294. PNG │ │ │ ├ ─ ─ Apple - touch - startup - image - 768 x1004. PNG │ │ │ ├ ─ ─ browserconfig. XML │ │ │ ├ ─ ─ a squadron - 228 x228. PNG │ │ │ ├ ─ ─ The favicon - 16 x16. PNG │ │ │ ├ ─ ─ the favicon - 32 x32. PNG │ │ │ ├ ─ ─ the favicon. Ico │ │ │ ├ ─ ─ firefox_app_128x128. PNG │ │ │ ├ ─ ─ Firefox_app_512x512.png │ │ ├─ Firefox_app_60x60.png │ │ ├─ Manifest.json │ │ ├─ Manifest.webApp │ │ ├─ Mstile - 144 x144. PNG │ │ │ ├ ─ ─ mstile - 150 x150. PNG │ │ │ ├ ─ ─ mstile - 310 x150. PNG │ │ │ ├ ─ ─ mstile - 310 x310. PNG │ │ │ ├ ─ ─ X70 mstile - 70. The PNG │ │ │ ├ ─ ─ another dual - browser - 50 x50. PNG │ │ │ └ ─ ─ another dual - browser - manifest. Json │ │ ├ ─ ─ js │ │ │ ├ ─ ─ Analytics. 45 eff9ff7d6c7c1e3c3d4184fdbbed90. Js │ │ │ ├ ─ ─ app. 30334 b5124fa6e221464. Js │ │ │ ├ ─ ─ App. 30334 b5124fa6e221464. Js. Map │ │ │ ├ ─ ─ app - legacy. 560 ef247e6649c0c24d0. Js │ │ │ ├ ─ ─ App - legacy. 560 ef247e6649c0c24d0. Js. Map │ │ │ ├ ─ ─ confetti. 1152197 f8c58a1b40b34. Js │ │ │ ├ ─ ─ Confetti. 1152197 f8c58a1b40b34. Js. Map │ │ │ ├ ─ ─ confetti - legacy. 8 e9093b414ea8aed46e5. Js │ │ │ ├ ─ ─ Confetti - legacy. 8 e9093b414ea8aed46e5. Js. Map │ │ │ ├ ─ ─ precache - manifest. F774c437974257fc8026 ca1bc693655c. Js │ │ │ ├ ─ ─ styles - legacy. D833997e3e3f91af64e7. Js │ │ │ ├ ─ ─ styles - legacy. D833997e3e3f91af64e7. Js. The map │ │ │ ├ ─ ─ vendors ~ confetti ~ vue. 03 b9213ce186db5518ea. Js │ │ │ ├ ─ ─ vendors ~ confetti ~ vue. 03 b9213ce186db5518ea. Js. Map │ │ │ ├ ─ ─ vendors ~ confetti ~ vue - legacy. E31223849ab7fea17bb8. Js │ │ │ ├ ─ ─ Vendors ~ confetti ~ vue - legacy. E31223849ab7fea17bb8. Js. Map │ │ │ └ ─ ─ workbox - catch - handler. Js │ │ ├ ─ ─ the manifest. Json │ │ ├ ─ ─ ├─ ├─ manifest-legal. json │ │ ├─ ├─ manifest-legal. HTML │ │ ├─ ├─ manifest-legal. HTML │ │ ├─ ├─ manifest-legal. json │ │ ├─ ├─ ├─ ├─ ├─ ├─ ├─ manifest-legal. HTML │ │ ├─ ├─ Workbox - catch - handler. Js │ ├ ─ ─ the favicon. Ico - > dist/img/favicons/favicon. Ico │ ├ ─ ─ index. The PHP │ ├ ─ ─ offline. HTML │ ├ ─ ─ Offline. SVG │ └ ─ ─ sw. Js ├ ─ ─ webpack.com mon. Js ├ ─ ─ webpack. Dev. Js ├ ─ ─ webpack. Prod. Js ├ ─ ─ webpack. Settings. The js └ ─ ─ yarn.lockCopy the code

INJECTING SCRIPT & CSS TAGS IN YOUR HTML

With the Webpack configuration shown here,

If you are not using a Craft CMS or a system with a template engine and want to inject these tags into HTML, you need to use the HtmlWebpackPlugin to do this. This configuration is already included, you just need to add a configuration to tell it to inject tags into HTML.

CRAFT CMS 3 INTEGRATION WITH THE TWIGPACK PLUGIN

If you are not using Craft CMS 3, you can skip this section, it just provides some useful integration information.

I wrote a free plug-in called Twigpack that makes it easy to integrate our WebPack build setup with Craft CMS 3.

It processes manifest.json files to inject entry points into Twig templates, and even to handle performing old/new module injection, asynchronous CSS loading, and more patterns.

It will make the Webpack4 configuration described here very simple.

To include CSS, I do this:

<! --# if expr="$HTTP_COOKIE=/critical\-css\=1/" -->
    {{ craft.twigpack.includeCssModule("styles.css".false) }}
<!--# else -->
    <script>
        Cookie.set("critical-css".'1', { expires: "7D", secure: true });
    </script>
    {{ craft.twigpack.includeCriticalCssTags() }}

    {{ craft.twigpack.includeCssModule("styles.css".true) }} {{ craft.twigpack.includeCssRelPreloadPolyfill() }} <! --# endif -->
Copy the code

If the critical-CSS cookie is not set, we set cookies via TinyCookie, including our critical CSS, and asynchronously load the site CSS. For more information about Critical CSS, see the Implementing Critical CSS on Your Website article.

To provide our javascript, we simply do the following:

{{ craft.twigpack.includeSafariNomoduleFix() }}
{{ craft.twigpack.includeJsModule("app.js".true)}}Copy the code

The second argument, true, tells it to load the JavaScript asynchronous module, so the generated HTML looks like this:

<script>
!function(){var e=document,t=e.createElement("script");if(! ("noModule"in t)&&"onbeforeload"int){var n=! 1; e.addEventListener("beforeload".function(e){if(e.target===t)n=! 0;else if(! e.target.hasAttribute("nomodule") | |! n)return; e.preventDefault()},! 0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();
</script>
<script type="module" src="http://example.test/dist/js/app.273e88e73566fecf20de.js"></script>
<script nomodule src="http://example.test/dist/js/app-legacy.95d36ead9190c0571578.js"></script>
Copy the code

See the Twigpack documentation for details.

Here is the full config/twigpack.php file I used, please note that it has my native Settings running inside the Homestead VM, which may be different from yours:

return [
    // Global settings
    The '*' => [
        // If `devMode` is on, use webpack-dev-server to all for HMR (hot module reloading)
        'useDevServer'= >false,
        // The JavaScript entry from the manifest.json to inject on Twig error pages
        'errorEntry'= >' ',
        // Manifest file names
        'manifest'= > ['legacy'= >'manifest-legacy.json'.'modern'= >'manifest.json',
        ],
        // Public server config
        'server'= > ['manifestPath'= >'/dist/'.'publicPath'= >'/',
        ],
        // webpack-dev-server config
        'devServer'= > ['manifestPath'= >'http://localhost:8080/'.'publicPath'= >'http://localhost:8080/',
        ],
        // Local files config
        'localFiles'= > ['basePath'= >'@webroot/'.'criticalPrefix'= >'dist/criticalcss/'.'criticalSuffix'= >'_critical.min.css',
        ],
    ],
    // Live (production) environment
    'live' => [
    ],
    // Staging (pre-production) environment
    'staging' => [
    ],
    // Local (development) environment
    'local' => [
        // If `devMode` is on, use webpack-dev-server to all for HMR (hot module reloading)
        'useDevServer'= >true,
        // The JavaScript entry from the manifest.json to inject on Twig error pages
        'errorEntry'= >'app.js',
        // webpack-dev-server config
        'devServer'= > ['manifestPath'= >'http://localhost:8080/'.'publicPath'= >'http://192.168.10.10:8080/',]]];Copy the code

WRAPPING UP!

Wow, this is a pit, and when I first started researching WebPack, I quickly realized that it was a very powerful tool with very powerful features. How far you go depends on how deep you want to swim.

For the full source code for this article, see the Annotated-webpack-4-config repository.

I hope this article will help you digest it slowly and make it better.

FURTHER READING

  • A Gulp Workflow for Frontend Development Automation

  • Post-Mortem: Applied Image Optimization

  • Handling Errors Gracefully in Craft CMS

If you want to be notified about new articles, follow @nyStudio107 on Twitter.