Slim 4 - Compiling Assets with Webpack

21 Sep 2019

Table of contents

Requirements

Introduction

If you’ve ever been confused and overwhelmed about getting started with Webpack and asset compilation, you should read further. In this tutorial we are using Webpack to bundle (compile, combine and minimize) web assets like JavaScript modules and CSS files.

Directory structure

/                              the root of your project
/composer.json
/webpack.config.js             the main config file for Webpack
/templates/                    the twig templates and page specific assets
/templates/home/               
/templates/user/
/public/                       the webservers document root
/public/index.php              the front controller (application entry point)
/public/assets                 the compiled assets (webpack output)
/public/assets/manifest.json   the generated manifest file (required by the webpack twig extension)

Webpack setup

Create a new package.json file at the root of your project. This file lists the packages your project depends on:

{
    "name": "my-app",
    "version": "1.0.0",
    "license": "MIT",
    "private": true,
    "dependencies": {
    },
    "devDependencies": {
        "clean-webpack-plugin": "^3.0.0",
        "css-loader": "^3.2.0",
        "file-loader": "^4.2.0",
        "mini-css-extract-plugin": "^0.8.0",
        "optimize-css-assets-webpack-plugin": "^5.0.3",
        "terser-webpack-plugin": "latest",
        "webpack": "^4.40.2",
        "webpack-assets-manifest": "^3.1.1",
        "webpack-cli": "^3.3.9",
        "webpack-manifest-plugin": "^2.0.4"
    }
}

Create a new webpack.config.js file at the root of your project. This is the main config file for Webpack:

const path = require('path');
const webpack = require('webpack');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');

module.exports = {
    entry: {
        'home/home-index': './templates/home/home-index.js'
        // here you can add more entries for each page or global assets like jQuery and bootstrap
        // 'layout/layout': './templates/layout/layout.js'
    },
    output: {
        path: path.resolve(__dirname, 'public/assets'),
    },
    optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
    },
    performance: {
        maxEntrypointSize: 1024000,
        maxAssetSize: 1024000
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader']
            }
        ],
    },
    plugins: [
        new CleanWebpackPlugin(),
        new ManifestPlugin(),
        new MiniCssExtractPlugin({
            ignoreOrder: false
        }),
    ],
    watchOptions: {
        ignored: ['./node_modules/']
    },
    mode: "development"
};

They key part is the entry object: This tells Webpack to load the templates/home/home-index.js file and follow all of the require / import statements. It will then package everything together and - thanks to the first home/home-index key - output final home/home-index.js and home/home-index.css files into the public/assets/ directory. Later you can add more page-specific JavaScript or CSS to the entry object.

Note that Webpack uses the TerserWebpackPlugin to minify your JavaScript and the OptimizeCSSAssetsPlugin to minify your CSS files.

To install webpack and all dependencies, run:

npm install

Twig Webpack extension setup

Now we install the Twig Webpack extension with composer:

composer require fullpipe/twig-webpack-extension

Register the WebpackExtension Twig extension:

// $app is the Slim 4 App instance
// We need the basePath to configure the correct public assets path
$basePath = $app->getBasePath();

// ...

$twig->addExtension(new \Fullpipe\TwigWebpackExtension\WebpackExtension(
    // must be a absolute path
    '/var/www/example.com/public/assets/manifest.json',
    // url path for js
    $basePath . '/assets/',
    // url path for css
    $basePath . '/assets/'
));

Creating assets

Next, create a new templates/home/home-index.js file with some basic JavaScript and import some CSS with require:

require('./home-index.css');

alert('Hello World!');

Create the new templates/home/home-index.css file:

body {
    background-color: lightgray;
}

Create a twig template file templates/layout/layout.twig with this content:

<!DOCTYPE html>
<html>
    <head>
        <!-- ... -->
        
        {% block css %}{% endblock %}
    </head>
    <body>
        <!-- ... -->
        {% block content %}{% endblock %}

        {% block js %}{% endblock %}
    </body>
</html>

Create a twig template file templates/home/home-index.twig with this content:

{% extends "layout/layout.twig" %}

{% block css %}
    {% webpack_entry_css 'home/home-index' %}
{% endblock %}

{% block js %}
    {% webpack_entry_js 'time/time-index' %}
{% endblock %}

{% block content %}

Welcome

{% endblock %}

Notice: The ‘home/home-index’ must match the first key of the entry item in webpack.config.js.

The webpack_entry_css and webpack_entry_js will fetch the url from the webpack manifest.json file and outputs the appropriate html link tags. For example:

<link type="text/css" href="/assets/home/home-index.css" rel="stylesheet">
<script type="text/javascript" src="/assets/home/home-index.js"></script>  

Compiling assets

To build the assets for development, run:

npx webpack --mode=development

or just:

npx webpack

To compile and minify the assets for production, run:

npx webpack --mode=production

Congrats! You now have three new files:

Useful tips

Recompiling on change

Webpack can watch and recompile files whenever they change.

npx webpack --watch

To stop the webpack watch process, press Ctrl+C.

Read more:

jQuery setup

jQuery uses the global variable window.jQuery and the alias window.$. The problem ist that Webpack will wrap all modules within a closure function to protect the global scope. For this reason we have to bind the jQuery instance to the global scope manually.

To install jQuery, run:

npm install jquery

The browser must load jQuery before other jQuery plugins can be used. For this reason, I would recommend bundling jQuery into a generally available asset file.

Add a new weback entry in webpack.config.js:

module.exports = {
    entry: {
        'layout/layout': './templates/layout/layout.js',
        // ...
    },
    // ...
};

Bind jQuery to the global scope in your webpack entry point templates/layout/layout.js:

window.jQuery = require('jquery');
window.$ = window.jQuery;

Add the assets {% webpack_entry_css 'layout/layout' %} and {% webpack_entry_js 'layout/layout' %} to the Twig template layout/layout.twig:

<!DOCTYPE html>
<html>
    <head>
        <!-- ... -->
        
        {% webpack_entry_css 'layout/layout' %}
        
        {% block css %}{% endblock %}
        
        {% webpack_entry_js 'layout/layout' %}
    </head>
    <body>
        <!-- ... -->
        {% block content %}{% endblock %}

        {% block js %}{% endblock %}
    </body>
</html>

Bootstrap setup

Bootstrap 4 uses jQuery and Popper.js for JavaScript components (like modals, tooltips, popovers etc). You have to setup jQuery for Webpack first.

To install Bootstrap, run:

npm install bootstrap

Import boostrap in a global available webpack entry point like: templates/layout/layout.js:

window.jQuery = require('jquery');
window.$ = window.jQuery;

require('bootstrap');
require('popper.js');
require('bootstrap/dist/css/bootstrap.css');

Fontawesome setup

Fontawesome is the world’s most popular and easiest to use icon set.

To install Fontawesome, run:

npm install @fortawesome/fontawesome-free

We also need the file-loader for the webfonts:

npm install file-loader --save-dev

We want to copy the Fontawesome fonts automatically into the assets directory. The file-loader copies the font files, to the build directory.

To install the file-loader, run:

npm install file-loader --save-dev

Import Fontawesome in a global available webpack entry like: templates/layout/layout.js:

You can import all fontawesome icons…

require('@fortawesome/fontawesome-free/css/all.min.css');

… or you can import only a specific set of icons:

require('@fortawesome/fontawesome-free/css/fontawesome.css');
require('@fortawesome/fontawesome-free/css/v4-shims.css');
require('@fortawesome/fontawesome-free/css/regular.css');
require('@fortawesome/fontawesome-free/css/solid.css');
require('@fortawesome/fontawesome-free/css/brands.css');

Note: We don’t install the js dependencies here, because it would blow up your js build to >1 MB of usless javascript. We only need the plain css and webfont files for fontawesome.

To copy the fonts into the assets/webfonts/ directory, add this rule to your webpack.config.js file:

 module: {
        rules: [
            // ...
            {
                test: /\.(ttf|eot|svg|woff|woff2)(\?[\s\S]+)?$/,
                include: path.resolve(__dirname, './node_modules/@fortawesome/fontawesome-free/webfonts'),
                use: {
                    loader: 'file-loader',
                    options: {
                        name: '[name].[ext]',
                        outputPath: 'webfonts',
                        publicPath: '../webfonts',
                    },
                }
            },
        ],
    },

SweetAlert2 setup

SweetAlert2 is a beautiful, responsive, customizable and accessible replacement for JavaScript’s popup boxes.

To install SweetAlert2, run:

npm install sweetalert2

Import the sweetalert2 module and bind Swal to the global scope:

window.Swal = require('sweetalert2');

Usage:

Swal.fire(
  'Good job!',
  'You clicked the button!',
  'success'
);

DataTables setup

DataTables.net is a very flexible table plug-in for jQuery.

You have to setup jQuery for Webpack first.

To install DataTables, run:

npm install datatables.net-bs4 datatables.net-responsive-bs4 datatables.net-select-bs4

Add a new weback entry in webpack.config.js:

module.exports = {
    entry: {
        'layout/layout': './templates/layout/layout.js',     // <- jquery is loaded here
        'layout/datatables': './templates/layout/datatables.js', // <-- add this line
        'user/user-list': './templates/user/user-list.js', // <-- add this line
        // other pages ...
    },
    // ...
};

Import datatables in a global available webpack entry point like: templates/layout/datatables.js:

window.$.fn.DataTable = require('datatables.net-bs4');
require('datatables.net-bs4/css/dataTables.bootstrap4.css');

Create a new Twig template: templates/user/user-list.twig:

{% extends "layout/layout.twig" %}

{% block css %}
    {% webpack_entry_css 'layout/datatables' %}
{% endblock %}

{% block js %}
    {% webpack_entry_js 'layout/datatables' %}
    {% webpack_entry_js 'user/user-list' %}
{% endblock %}

{% block content %}
  <div id="content" class="container">
        <div class="row">
            <div class="col-md-12">
                <h1><i class="fas fa-user"></i> User list</h1>
                <hr>
                <table id="my-data-table" class="table table-striped table-bordered dt-responsive nowrap dataTable no-footer dtr-inline collapsed">
                    <thead>
                    <tr>
                        <th>Username</th>
                        <th>E-Mail</th>
                        <th>First name</th>
                        <th>Last name</th>
                    </tr>
                    <tfoot></tfoot>
                </table>
                <p></p>
            </div>
        </div>
{% endblock %}

Call this single function in templates/user/user-list.js:

$(function() {
    $('#my-data-table').DataTable();
});

Babel setup

Installation:

npm install --save-dev babel-loader @babel/core @babel/preset-env webpack

Add this rule to your webpack.config.js file:

 module: {
        rules: [
            // ...
            {
                test: /\.js$/,
                exclude: path.resolve('node_modules'),
                use: [{
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            ['@babel/preset-env']
                        ]
                    }
                }]
            },
        ],
    },