webpack hot module replacement: css without refresh

17.1k views Asked by At

So far I've been using livereload so that whenever I change JS or templates, the page refreshes, and when I change CSS, it would hotswap the new CSS without a refresh.

I'm trying out webpack now and nearly got to the same behaviour, with one exception: when the CSS changes, it refreshes the whole window. Is it possible to make it hotswap the CSS without refresh?

Config so far:

var webpackConfig = {
    entry: ["webpack/hot/dev-server", __dirname + '/app/scripts/app.js'],
    debug: true,
    output: {
        path: __dirname + '/app',
        filename: 'scripts/build.js'
    },
    devtool: 'source-map',
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new htmlWebpackPlugin({
            template: __dirname + '/app/index.html',
            inject: 'body',
            hash: true,
            config: config
        }),
        new webpack.ProvidePlugin({
            'angular': 'angular'
        }),
        new ExtractTextPlugin("styles.css")
    ],
    module: {
        loaders: [
            {
                test: /\.scss$/,
                loader: "style!css!sass?includePaths[]=" + __dirname + "/app/bower_components/compass-mixins/lib&includePaths[]=" + __dirname + '/instance/sth/styles&includePaths[]=' + __dirname + '/app/scripts'
            }
        ]
    }
};
6

There are 6 answers

1
Juho Vepsäläinen On

This is one of the downsides of using ExtractTextPlugin as stated in the project README. You can resolve the issue by splitting up your configuration. Ie. have separate configuration for development without it and one for production with it.

0
Ido On

In webpack 5 using preact and prefresh I changed the hot: true to hotOnly: true and the styles changed without the reload.

0
Hatch On

There's actually a simple way to do this. I'm using sass-loader with extract-text-plugin that produces css files.

What you need to do is add id to your css include

  <link id="js-style" type="text/css" rel="stylesheet" href="/static/main.css">

Now, you need to ensure when HMR happens you update url with current version/timestamp. You can do this in such way:

import '../style/main.scss'
if (module.hot) {
  module.hot.accept('../style/main.scss', () => {
    const baseStyle = window.document.getElementById('js-style')
    baseStyle.setAttribute('href', '/static/main.css?v=' + new Date().valueOf)
  })
}

So whenever css changes, we'll fix url of css include to reload it.

0
urip On

Although ExtractTextPlugin states "No Hot Module Replacement" in its README section, I fixed this by manually updating the CSS files through the BrowserSync API.

I used gaze to listen for any changes in my CSS files, and then used BrowserSync to update them.

    npm install gaze

You can also easily do so by editing your build script with something of the following:

  const { Gaze } = require('gaze');

  // Your own BrowserSync init
  browserSync.init({
    ...
  }, resolve);

  // Create a watcher:
  // You can watch/reload specific files according to your build/project structure
  const gaze = new Gaze('**/*.css');
  gaze.on('all', () => bs.reload('*.css'));

Hope that helps.

1
ddemakov On

You can use 'css-hot-loader' to enable HMR for your extracted css. It works for me perfectly.

1
TetraDev On

It is now possible to use angular2, webpack with hot module replacement, sass sourcemaps, and externally loaded css. It tooks me days of playing with it but I got it working!

The dependencies are style-loader, css-loader, and sass-loader (if using sass, if not, the sass loader can be removed)

I use ExtractTextPlugin for production mode to emit actual .css files.

NOTE: To get this to work, I don't use the stylesUrl property, instead I import the .scss file outside of the @Component decorator so that the styles load in the global context, rather than scoped by component.

This configuration allows for Hot Module replacement with SCSS files using webpack dev server, and extracttextplugin for production mode to emit actual .css files.

Here's my working config

{
        test: /\.(scss)$/,
        use:
          isDevServer ? [
              {
                loader: 'style-loader',
              },             
              {
                loader: 'css-loader',
                options: { sourceMap: true }
              },
              {
                loader: 'postcss-loader',
                options: { postcss: [AutoPrefixer(autoPrefixerOptions)], sourceMap: true }
              },
              {
                loader: 'sass-loader',
                options: { sourceMap: true }
              },
              {
                loader: 'sass-resources-loader',
                options: {
                  resources: [
                    './src/assets/styles/variables.scss',
                    './src/assets/styles/mixins.scss']
                }
              }, 
              /**
               * The sass-vars-loader will convert the 'vars' property or any module.exports of 
               * a .JS or .JSON file into valid SASS and append to the beginning of each 
               * .scss file loaded.
               *
               * See: https://github.com/epegzz/sass-vars-loader
               */
              {
                loader: '@epegzz/sass-vars-loader?',
                options: querystring.stringify({
                  vars: JSON.stringify({
                    susyIsDevServer: susyIsDevServer
                  })
                })
              }] : // dev mode
          ExtractTextPlugin.extract({
            fallback: "css-loader",
            use: [
              {
                loader: 'css-loader',
                options: { sourceMap: true }
              },
              {
                loader: 'postcss-loader',
                options: { postcss: [AutoPrefixer(autoPrefixerOptions)], sourceMap: true }
              },
              {
                loader: 'sass-loader',
                options: { sourceMap: true }
              },
              {
                loader: 'sass-resources-loader',
                options: {
                  resources: [
                    './src/assets/styles/variables.scss',
                    './src/assets/styles/mixins.scss']
                }
              }, {
                loader: '@epegzz/sass-vars-loader?',
                options: querystring.stringify({
                  vars: JSON.stringify({
                    susyIsDevServer: susyIsDevServer
                  })
                  // // Or use 'files" object to specify vars in an external .js or .json file
                  // files: [
                  //    path.resolve(helpers.paths.appRoot + '/assets/styles/sass-js-variables.js')
                  // ],
                })
              }],
            publicPath: '/' // 'string' override the publicPath setting for this loader
          })
      },

Then, in your component, for example, app.component.ts, you would require your app.style.scss file OUTSIDE of the @Component decorator.

That is the trick. This will not work if you load styles the "angular way" with stylesUrl. Doing it this way will allow you to lazy load .css stylesheets for components that are lazy loaded, making initial load time even faster.

app.component.css

/*
 * THIS IS WHERE WE REQUIRE CSS/SCSS FILES THAT THIS COMPONENT NEEDS
 *
 * Function: To enable so-called "Lazy Loading" CSS/SCSS files "on demand" as the app views need them.
 * Do NOT add styles the "Angular2 Way" in the @Component decorator ("styles" and "styleUrls" properties)
 */
    import './app.style.scss'

/**
 * AppComponent Component
 * Top Level Component
 */
@Component({
   selector: 'body',
   encapsulation: ViewEncapsulation.None,
   host: { '[class.authenticated]': 'appState.state.isAuthenticated' },
   templateUrl: './app.template.html'
})

I have had no problems running this setup. Here you go!

sass_sourcemaps _hmr _wds

Updated 08/2017: Improved config for webpack 3+ schema requirements, and to work with Angular 4 AOT compilation.