Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

extract-text-webpack-plugin

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ever since we started requiring CSS from JS, we've had this annoying problem: when the page loads... just for a second... there's no CSS! Oof! Ugly!

This is because the CSS is packaged inside our JavaScript... so we need to wait for it to download and be executed.

This is ok for development, but we cannot have this on production. The fix involves one of the most important plugins in all of Webpack: the extract-text-webpack-plugin. It has a weird name.... but has one simple job: it outputs a real CSS file, instead of embedding CSS in JavaScript.

Tip

In Webpack 4 you can use the beta version extract-text-webpack-plugin@4.0.0-beta.0 or there is a new way to extract CSS to files with https://webpack.js.org/plugins/mini-css-extract-plugin/

Let's get it rocking! Find your terminal and run

yarn add extract-text-webpack-plugin --dev

Current Setup: style-loader

Now open up webpack.config.js. Remember: CSS is processed via loaders. If a file ends in .css, it goes through the css-loader and then the style-loader:

125 lines webpack.config.js
... lines 1 - 7
const styleLoader = {
loader: 'style-loader',
options: {
sourceMap: true
}
};
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: true
}
};
... lines 20 - 32
module.exports = {
... lines 34 - 43
module: {
rules: [
... lines 46 - 55
{
test: /\.css$/,
use: [
styleLoader,
cssLoader,
]
},
... lines 63 - 93
]
},
... lines 96 - 123
};

For Sass, it's basically the same: the sass-loader, resolve-url-loader, then the same css-loader and style-loader:

125 lines webpack.config.js
... lines 1 - 7
const styleLoader = {
loader: 'style-loader',
options: {
sourceMap: true
}
};
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: true
}
};
const sassLoader = {
loader: 'sass-loader',
options: {
sourceMap: true
}
};
const resolveUrlLoader = {
loader: 'resolve-url-loader',
options: {
sourceMap: true
}
};
module.exports = {
... lines 34 - 43
module: {
rules: [
... lines 46 - 62
{
test: /\.scss$/,
use: [
styleLoader,
cssLoader,
resolveUrlLoader,
sassLoader,
]
},
... lines 72 - 93
]
},
... lines 96 - 123
};

The style-loader is the key: it embeds the CSS so that it's added to the page as a style tag. Basically, we need to replace the style-loader with something that, instead, outputs a real CSS file. That's exactly what the extract-text-webpack-plugin does!

Bring in the package with const ExtractTextPlugin = require('extract-text-webpack-plugin'):

133 lines webpack.config.js
... lines 1 - 3
const ExtractTextPlugin = require('extract-text-webpack-plugin');
... lines 5 - 133

The Loaders: ExtractTextPlugin.extract()

Now, under the loaders for .css files, remove the two loaders and, instead, add ExtractTextPlugin.extract() and pass that some options:

133 lines webpack.config.js
... lines 1 - 3
const ExtractTextPlugin = require('extract-text-webpack-plugin');
... lines 5 - 33
module.exports = {
... lines 35 - 44
module: {
rules: [
... lines 47 - 56
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
... lines 60 - 64
}),
},
... lines 67 - 99
]
},
... lines 102 - 131
};

First, use set to cssLoader. And then, fallback set to styleLoader:

133 lines webpack.config.js
... lines 1 - 3
const ExtractTextPlugin = require('extract-text-webpack-plugin');
... lines 5 - 33
module.exports = {
... lines 35 - 44
module: {
rules: [
... lines 47 - 56
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: [
cssLoader
],
... line 63
fallback: styleLoader
}),
},
... lines 67 - 99
]
},
... lines 102 - 131
};

Ignore the fallback key for a moment. Basically, the extract() function is a fancy way to prepend the loaders in use with a special extract-text-webpack-plugin loader. Thanks to this, CSS files will be processed through css-loader and then through this new extract text loader.

The styleLoader, which is set on fallback, is not used at all anymore:

133 lines webpack.config.js
... lines 1 - 3
const ExtractTextPlugin = require('extract-text-webpack-plugin');
... lines 5 - 33
module.exports = {
... lines 35 - 44
module: {
rules: [
... lines 47 - 56
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: [
cssLoader
],
// use this, if CSS isn't extracted
fallback: styleLoader
}),
},
... lines 67 - 99
]
},
... lines 102 - 131
};

Well, actually, this is one of the least important, but most confusing parts about the plugin. Later, when we talk about code splitting, I'll explain what the fallback option really does. But for now, the styleLoader is no longer used. So, no more style tags!

Repeat this for Sass. I'll copy the three loaders and then say ExtractTextPlugin.extract(), passing that use set to those 3 loaders and fallback set again to styleLoader:

133 lines webpack.config.js
... lines 1 - 3
const ExtractTextPlugin = require('extract-text-webpack-plugin');
... lines 5 - 33
module.exports = {
... lines 35 - 44
module: {
rules: [
... lines 47 - 66
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
use: [
cssLoader,
resolveUrlLoader,
sassLoader,
],
fallback: styleLoader
}),
},
... lines 78 - 99
]
},
... lines 102 - 131
};

Adding the Plugin

The last step is to activate all of this down in the plugins section. Add new ExtractTextPlugin() and pass it a special name: [name].css:

133 lines webpack.config.js
... lines 1 - 33
module.exports = {
... lines 35 - 101
plugins: [
... lines 103 - 123
new ExtractTextPlugin('[name].css'),
],
... lines 127 - 131
};

Let's see what this does! Find your webpack tab and restart the dev server:

./node_modules/.bin/webpack-dev-server --hot

When it finishes, scroll up to the output. In addition to login.js, there is a login.css! And a layout.css and a rep_log.css. Our CSS is no longer packaged inside JavaScript: each entry now has its own CSS file!

Tip

If a JavaScript entry file does not require any CSS, no .css file will be output by Webpack.

This will fix our CSS flashing problem! But... with a downside: we now need to manually include link tags along with our script tags. In base.html.twig, add a link tag for build/layout.css:

... lines 1 - 2
<head>
... lines 4 - 10
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('build/layout.css') }}">
{% endblock %}
... lines 14 - 15
</head>
... lines 17 - 106

Copy that. We need to do this again on our two pages: app/Resources/FOSUserBundle/views/Security/login.html.twig. Override the block stylesheets, call parent() and add the link tag to login.css:

... lines 1 - 10
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('build/login.css') }}">
{% endblock %}
... lines 16 - 72

Do all of this again in index.html.twig. This time we need to point to rep_log.css:

... lines 1 - 62
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('build/rep_log.css') }}">
{% endblock %}

I think we're ready! Refresh the page. Yes! No CSS delay! View the source and click on rep_log.css. Yep! It's a beautiful, traditional CSS file.

We Killed HMR!

But... I have some bad news. We just killed hot module replacement! If you make a change and move over to your browser... it says "Nothing hot updated" and "App is up to date".

Yep, extract-text-webpack-plugin and HMR are incompatible. Boooo! There is a plugin to make this all work - css-hot-loader, but it's pretty young and I haven't tested it yet. If you like HMR, try it out!

But, how is it possible that two super important features like extract text and HMR don't work together?! Well, the official answer is that extract-text-webpack-plugin should only be used for your production build - not during development. We'll talk about production builds next.

But I don't like that! If we don't use extract-text-webpack-plugin while developing, then my site will look great... even if I completely forget to add my link tag! I might only discover a page is ugly after deploying it to production. That's why I always enable the plugin. But, if you like HMR and you can't get it to work with that other plugin, disabling it during development is a valid option.

For us, I'm going to stop using the webpack-dev-server. At the top of webpack.config.js, set useDevServer to false:

133 lines webpack.config.js
... lines 1 - 5
const useDevServer = false;
... lines 7 - 133

And then, in app/config/config.yml, comment out the base_url stuff:

... lines 1 - 3
framework:
... lines 5 - 8
# assets:
# base_url: 'http://localhost:8080'
... lines 11 - 37

Yep, we'll use the tried and true webpack --watch:

./node_modules/.bin/webpack --watch

Leave a comment!

6
Login or Register to join the conversation
GDIBass Avatar
GDIBass Avatar GDIBass | posted 5 years ago

Had to install the extract-text-webpack-plugin alpha 4.0 plugin for webpack 4.0. (Kind of concerning that this was reported 3+ mo's ago and still no official release, but oh well).

yarn add extract-text-webpack-plugin@next --dev

1 Reply
Default user avatar
Default user avatar Thomas Talbot | posted 5 years ago

Hi,
Thanks for the course. :)

There is a plugin for HMR with extract-text-webpack-plugin : https://www.npmjs.com/packa....
Apparently, it works nicely. :)

1 Reply
GDIBass Avatar

Thank you!

Reply

Hey Thomas Talbot!

Yes! This is the one I still want to try! Even a few months ago, it was VERY young and messy. But it looks better now. I would love to include it in Webpack Encore :).

Cheers!

Reply
Daniel G. Avatar
Daniel G. Avatar Daniel G. | posted 4 years ago

Note that this plugin is now not supported:

> https://github.com/webpack-...

Instead, this plugin

> https://github.com/webpack-...

is recommended. I checked `css-hot-loader`. It was super easy: enough

npm install css-hot-loader --save-dev

and add line

'css-hot-loader',

before MiniCssExtractPlugin like in docs:

> https://www.npmjs.com/packa...

Reply

Cool. Thanks for sharing it Daniel!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial explains the concepts of an old version of Webpack using an old version of Symfony. The most important concepts are still the same, but you should expect significant differences in new versions.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "symfony/symfony": "3.3.*", // v3.3.16
        "twig/twig": "2.10.*", // v2.10.0
        "doctrine/orm": "^2.5", // v2.7.0
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.5
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.3
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.4.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "^2.0", // v2.1.2
        "doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.6
        "symfony/phpunit-bridge": "^3.0" // v3.3.5
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "dependencies": [],
    "devDependencies": {
        "babel-core": "^6.25.0", // 6.25.0
        "babel-loader": "^7.1.1", // 7.1.1
        "babel-plugin-syntax-dynamic-import": "^6.18.0", // 6.18.0
        "babel-preset-env": "^1.6.0", // 1.6.0
        "bootstrap-sass": "^3.3.7", // 3.3.7
        "clean-webpack-plugin": "^0.1.16", // 0.1.16
        "copy-webpack-plugin": "^4.0.1", // 4.0.1
        "core-js": "^2.4.1", // 2.4.1
        "css-loader": "^0.28.4", // 0.28.4
        "extract-text-webpack-plugin": "^3.0.0", // 3.0.0
        "file-loader": "^0.11.2", // 0.11.2
        "font-awesome": "^4.7.0", // 4.7.0
        "jquery": "^3.2.1", // 3.2.1
        "lodash": "^4.17.4", // 4.17.4
        "node-sass": "^4.5.3", // 4.5.3
        "resolve-url-loader": "^2.1.0", // 2.1.0
        "sass-loader": "^6.0.6", // 6.0.6
        "style-loader": "^0.18.2", // 0.18.2
        "sweetalert2": "^6.6.6", // 6.6.6
        "webpack": "^3.4.1", // 3.4.1
        "webpack-chunk-hash": "^0.4.0", // 0.4.0
        "webpack-dev-server": "^2.6.1", // 2.6.1
        "webpack-manifest-plugin": "^1.2.1" // 1.2.1
    }
}
userVoice