How do I fingerprint images and other static assets in Ionic for cache busting?

Viewed 6087

I have extended default web pack config in Ionic v3 for forcing cache busting.

I am able to fingerprint generated JavaScript artifacts, but I am unable to fingerprint images and JSON files under the assets folder. I took Help from Bundled files and cache-busting.

An excerpt of webpack config.js

module.exports = {
  // ...
  output: {
    filename: '[name].[chunkhash].js',
    chunkFilename: '[name].[chunkhash].js',
  },
  plugins: [
    new WebpackChunkHash({algorithm: 'md5'}) // 'md5' is default value
  ]
}

The above is the approach for fingerprinting JavaScript bundles, and it's working fine. I want to add hashes/fingerprint images and JSON files inside the assets folder. I used the same approach for images also, but it did not work.

I have extended webpack config.js and added a new rule for images. By default webpack directly copies the images and assets to the output folder.

Copy Config.js

module.exports = {
  copyAssets: {
    src: ['{{SRC}}/assets/**/*'],
    dest: '{{WWW}}/assets'
  },
  copyIndexContent: {
    src: ['{{SRC}}/index.html', '{{SRC}}/manifest.json', '{{SRC}}/service-worker.js'],
    dest: '{{WWW}}'
  },
  copyFonts: {
    src: ['{{ROOT}}/node_modules/ionicons/dist/fonts/**/*', '{{ROOT}}/node_modules/ionic-angular/fonts/**/*'],
    dest: '{{WWW}}/assets/fonts'
  },

Here images and other assets are directly copied. I have added a new rule in extended webpack.config.js, but the build process is ignoring it. How do I fix this issue?

Excerpt of webpack config.js

 {
        test: /\.(png|jpg|gif)$/,
        loader: 'file-loader',
        options: {

            name:'[name].[hash].[ext]',//adding hash for cache busting
            outputPath:'assets/imgs',
            publicPath:'assets/imgs'


        },

entire Webpack.config.js

/*
 * The webpack config exports an object that has a valid webpack configuration
 * For each environment name. By default, there are two Ionic environments:
 * "dev" and "prod". As such, the webpack.config.js exports a dictionary object
 * with "keys" for "dev" and "prod", where the value is a valid webpack configuration
 * For details on configuring webpack, see their documentation here
 * https://webpack.js.org/configuration/
 */

var path = require('path');
var webpack = require('webpack');
var ionicWebpackFactory = require(process.env.IONIC_WEBPACK_FACTORY);

var ModuleConcatPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
var PurifyPlugin = require('@angular-devkit/build-optimizer').PurifyPlugin;

var optimizedProdLoaders = [
  {
    test: /\.json$/,
    loader: 'json-loader'
  },
  {
    test: /\.js$/,
    loader: [
      {
        loader: process.env.IONIC_CACHE_LOADER
      },

      {
        loader: '@angular-devkit/build-optimizer/webpack-loader',
        options: {
          sourceMap: true
        }
      },
    ]
  },
  {
    test: /\.ts$/,
    loader: [
      {
        loader: process.env.IONIC_CACHE_LOADER
      },

      {
        loader: '@angular-devkit/build-optimizer/webpack-loader',
        options: {
          sourceMap: true
        }
      },
      {
        test: /\.(png|jpg|gif)$/,
        loader: 'file-loader',
        options: {

            name:'[name].[hash].[ext]',
            outputPath:'assets/imgs',
            publicPath:'assets/imgs'
        },
      },


      {
        loader: process.env.IONIC_WEBPACK_LOADER
      }
    ]
  }
];

function getProdLoaders() {
  if (process.env.IONIC_OPTIMIZE_JS === 'true') {
    return optimizedProdLoaders;
  }
  return devConfig.module.loaders;
}

var devConfig = {
  entry: process.env.IONIC_APP_ENTRY_POINT,
  output: {
    path: '{{BUILD}}',
    publicPath: 'build/',
    filename: '[name].js',
    devtoolModuleFilenameTemplate: ionicWebpackFactory.getSourceMapperFunction(),
  },
  devtool: process.env.IONIC_SOURCE_MAP_TYPE,

  resolve: {
    extensions: ['.ts', '.js', '.json'],
    modules: [path.resolve('node_modules')]
  },

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: 'json-loader'
      },
      {
        test: /\.ts$/,
        loader: process.env.IONIC_WEBPACK_LOADER
      },
      {
      test: /\.(jpg|png)$/,
         use: {
         loader: "file-loader",
         options: {
         name: "[name].[hash].[ext]",
         outputPath:'assets/imgs',
         publicPath:'assets/imgs'

    },
  }},
    ]
  },

  plugins: [
    ionicWebpackFactory.getIonicEnvironmentPlugin(),
    ionicWebpackFactory.getCommonChunksPlugin()
  ],

  // Some libraries import Node.js modules but don't use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  node: {
    fs: 'empty',
    net: 'empty',
    tls: 'empty'
  }
};

var prodConfig = {
  entry: process.env.IONIC_APP_ENTRY_POINT,
  output: {
    path: '{{BUILD}}',
    publicPath: 'build/',
    filename: '[name].js',
    devtoolModuleFilenameTemplate: ionicWebpackFactory.getSourceMapperFunction(),
  },
  devtool: process.env.IONIC_SOURCE_MAP_TYPE,

  resolve: {
    extensions: ['.ts', '.js', '.json'],
    modules: [path.resolve('node_modules')]
  },

  module: {
    loaders: getProdLoaders()
  },

  plugins: [
    ionicWebpackFactory.getIonicEnvironmentPlugin(),
    ionicWebpackFactory.getCommonChunksPlugin(),
    new ModuleConcatPlugin(),
    new PurifyPlugin()
  ],

  // Some libraries import Node.js modules but don't use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  node: {
    fs: 'empty',
    net: 'empty',
    tls: 'empty'
  }
};


module.exports = {
  dev: devConfig,
  prod: prodConfig
}
4 Answers

Using Webpack 4 you should not need any additional plugins or loaders.

It will give you the naming option [contenthash].

Also, it looks like you have this block nested under the test: .ts block.

{
    test: /\.(png|jpg|gif)$/,
    loader: 'file-loader',
    options: {
        name:'[name].[hash].[ext]', // Adding hash for cache busting
        outputPath:'assets/imgs',
        publicPath:'assets/imgs'
    }
}

Ultimately, you can do something like this:

    // Copy static assets over with file-loader
    {
        test: /\.(ico)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'file-loader', options: {name: '[name].[contenthash].[ext]'},
    },
    {
        test: /\.(woff|woff2|eot|ttf|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'file-loader', options: {name: 'fonts/[name].[contenthash].[ext]'},
    },
    {
        test: /\.(jpg|gif|png|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'file-loader', options: {name: 'images/[name].[contenthash].[ext]'},
    }
]

Using [chunkhash] instead of content should still work, and if you're not using webpack4 do that, but otherwise for more information see this issue for an explanation.

For more help, read the long-term caching performance guide from Google and the latest caching documentation from Webpack.

the files copied via CopyPlugin will not pass to loaders. So even you have correct loader setting with hashname for images, it doesn't work.

but you can see https://github.com/webpack-contrib/copy-webpack-plugin#template

the CopyPlugin provide you a way to specify output name which can be set with hash:

module.exports = {
  plugins: [
    new CopyPlugin([
      {
        from: 'src/',
        to: 'dest/[name].[hash].[ext]',
        toType: 'template',
      },
    ]),
  ],
};

Eventually, I used gulp for fingerprinting static assets.

  1. Drop Angular Output hashing and build the application.ng build --prod --aot --output-hashing none .
  2. post-build execute a gulp script which would fingerprint all the assets and update the references.
npm i gulp gulp-rev gulp-rev-delete-original gulp-rev-collector

gulpfile.js

const gulp = require('gulp');
const rev = require('gulp-rev');
const revdel = require('gulp-rev-delete-original');
const collect = require('gulp-rev-collector');

// finger priniting static assets
gulp.task('revision:fingerprint', () => {
  return gulp
    .src([
      'dist/welcome/**/*.css',
      'dist/welcome/**/*.js',
      'dist/welcome/**/*.{jpg,png,jpeg,gif,svg,json,xml,ico,eot,ttf,woff,woff2}'
    ])
    .pipe(rev())
    .pipe(revdel())
    .pipe(gulp.dest('dist/welcome'))
    .pipe(rev.manifest({ path: 'manifest-hash.json' }))
    .pipe(gulp.dest('dist'));
});

gulp.task('revision:update-fingerprinted-references', () => {
  return gulp
    .src(['dist/manifest-hash.json', 'dist/**/*.{html,json,css,js}'])
    .pipe(collect())
    .pipe(gulp.dest('dist'));
});
gulp.task(
  'revision',
  gulp.series(
    'revision:fingerprint',
    'revision:update-fingerprinted-references'));

Add a new script in package.json

"gulp-revision": "gulp revision"

Execute npm run gulp-revision Post-build.

Solving Browser Cache Hell With Gulp-Rev

Using webpack-assets-manifest you can generate a map of asset names to fingerprinted names like so:

{
  "images/logo.svg": "images/logo-b111da4f34cefce092b965ebc1078ee3.svg"
}

Using this manifest you can then rename the assets in destination folder, and use the "correct", hash-inclusive src or href in your project.

The fix isn't framework-specific.

Related