Ionic-app-scripts: ionic2 cache busting and base href

Created on 20 Oct 2016  路  33Comments  路  Source: ionic-team/ionic-app-scripts

I am wanting to use ionic to build an app for android, ios, and the browser so that there is one repo and easy maintenance. There is a couple of things that I will need to accomplish this and they both are build step features.

Does ionic 2 support cache busting for the browser?

Does ionic 2 support setting the base href for the browser also?

If no to any of the two above are they planned. For example angular cli supports both of these and works wonderfully.

If not I may just have to add an extra build step to modify the index.html once it finishes the ionic build to accomplish these two features myself although it will add development time.

Thanks!

enhancement

Most helpful comment

Since you guys are now promoting the PWA support for ionic, I think that the cache busting is one of the essential parts that is missing during the build process. Without a way to version your assets, the browser version of the app is useless, unless you tinker into the build process and modify it, so it can version the assets.

Please reconsider reviewing this topic.

All 33 comments

Hello, thanks for using Ionic! Ionic 2 will work with cache busting and base href. These are standard features found on the web platform and will work with any web project (react, Angular 2, Ionic 2 etc etc). To add a base href you can add a base html tag to your index.html. For cache busting you can use any of the standard cache busting techniques, some of which are described here. Since this is more of a support question than a bug with ionic I am going to close this issue for now. Thanks again!

My question is specific to the build step not the features themselves.

Angular cli adds the versioning to the filenames directly in the build step.

I need to know if ionic implements a build step that adds a hash to the filenames not if I were to go in there and edit it myself. This needs to be automatic.

angular cli allows you to build the app with different base hrefs. I need to know if that is possible with ionic without touching the src Index.html.

I know how to implement these things but I believe these should be apart of the core build steps included with ionic cli.

Thanks

@jasekiw Hello, sorry for the misunderstanding! Cache busting by adding a hash to filenames is definitely something we can look into implementing. Also, handling of base hrefs is something we could also look into. Again, very sorry for misunderstanding the issue and closing it early. Thanks!

service worker generators should help in cache-busting. sometime back i tried sw-precache and it was working fine for cache busting.

@jasekiw,

At this time we have no plans of implementing that. Maybe in the future. Closing for now.

Thanks,
Dan

@danbucholtz I am running into an issue loading old js files with running Ionic 2 as a desktop app (which I know you are working on additional support for) and cache busting would help.

Since you guys are now promoting the PWA support for ionic, I think that the cache busting is one of the essential parts that is missing during the build process. Without a way to version your assets, the browser version of the app is useless, unless you tinker into the build process and modify it, so it can version the assets.

Please reconsider reviewing this topic.

Also running into this issue.

Woo woo! Got cache busting working in ionic 2!!! It's a bit hackish, but it works.

http://stackoverflow.com/questions/43415537/ionic-2-change-name-of-main-js-webpack-js-output-filename-setting/43429378#43429378

@jgw96 @danbucholtz there is an example @haydenbr made that should steer you guys on how you would do it for a quick feature release. Now that you guys are releasing desktop compatible components, now would be a good time to prioritize cache busting.

We're open to a well-tested PR.

Thanks,
Dan

Can anyone speak for this npm package? https://www.npmjs.com/package/ionic-cache-buster

Honestly a little afraid of hacking on the webpack configuration :worried:

@haydenbr Webpack Manifest Plugin does not generate manifes.json file with latest version of app-scripts.

Another vote for this feature. I'm deploying as an App but also as a PWA; hosted in AWS on S3 behind cloudfront with custom domain and ssl cert.

Without cache busting I will need to manually cache bust the js and css with one of the workarounds out there or manually on each release...

Am surprised with the push for Ionic and PWA that there aren't more people struggling, unless I'm missing something obvious; always possible!

+1

@dalibor983 We're working on upgrading to ionic 3 and are running into this issue. If I find a fix, I will post a solution.

solution to problem reported by @dalibor983:

http://stackoverflow.com/questions/43415537/ionic-2-change-name-of-main-js-webpack-js-output-filename-setting/43429378#43429378

It's again a bit hackish, but it works for running locally and for production. We are in the process of updated our app to ionic 3, and this is probably the solve we will use in production. I think my new weekend project will be a PR for a non-hackish solution for cache busting.

I insist on the implementation of this feature. I currently work on a project that has lazy-loading implemented, which we have problems with the cache of the Chrome browser. The browser keeps the js in memory until it is manually deleted with SHIFT + F5 or with deleting the temp memory.

I don't think this is something that ionic or ionic scripts should tackle.
I spent some time to scratch the surface of Service Workers to understand how caching actually works. To me hashing doesn't solve this issue, since you also cache your index.html if you go full offline-first.

Therefore I used the Service Worker API to implemented a way to tell the user that there is a newer version available, if you're going for that instant page load experience.
You can also have a little slower experience, that checks your network first (more in the article below).

Then after it worked, I wrote the steps down in https://medium.com/@zwacky/pwa-create-a-new-update-available-notification-using-service-workers-18be9168d717.
It's currently unlisted and probably still WIP.

I am in the exact same boat as @acahinton, deploying a lazy load AOT compiled PWA to AWS CloudFront with SSL. Most devices don't have a ton of caching issues with the TTL set to 1 day but some definitely have caching issues (Android) and I'd like to turn the cache up further and force a reload only when necessary.

Without lazy loading enabled it is possible to work around this with just webpack but as soon as you start working with lazy loading the whole process breaks down and as referenced above there doesn't seem to be any workaround. Please revisit!

Same problem here. Ionic PWA + AOT = 馃挬 (for now) without cache buster handling. Really hard to debug and clients get crazy as they don't have the same versions as us. :(

There is an ionic twitter pwa project that seems to handle all we want really well:
https://github.com/shprink/ionic-angular-twitter-pwa

If you look in the build script
https://github.com/shprink/ionic-angular-twitter-pwa/blob/master/scripts/build.sh

# Generate our SW manifest
ngu-sw-manifest --out $BUILDFOLDER"ngsw-manifest.json" \
--dist $BUILDFOLDER

He is generating a ngsw-manifest.json where there are hashes for all files so the service worker knows the latest version. It seems he uses angular service worker toolkit and I have no experience with it yet.

Could be interesting to find out whats going on there because lazy loading is active and cache busting seems to work as far as I checked.

I'm really stuck on service workers being a viable solution for anyone who has to support all browsers (most notably mobile IOS). Mobile IOS doesn't support notifications, badges, etc due to service workers but I'm still trying to roll out the same functionality and have the cache busting work so service workers as a cache busting mechanism is a no go until Apple joins in supporting PWAs.

In the meantime, I am using a Webpack build, so I adapted the solution @haydenbr posted into my first Webpack plugin and it is working very well for me. As long as you're on a Webpack build it should be adaptable. The one thing you may need to tweak is which index files get included.

A quick explanation:

  • Only impacts production builds
  • Webpack now outputs files as including the chunkhash
  • After the build, your index file is updated so the original .js includes are replaced with the newly built files
  • I included the code for a node script cacheBustCss.js, which I run from BitBucket pipelines after the build and uglification because webpack hasn't generated the assets. Simply call node ./scripts/build/cacheBustCss.js after the prod build is complete.

I hope this helps someone else and hopefully people can make it better!

package.json:

"config": {
    "ionic_bundler": "webpack",
    "ionic_webpack": "./webpack.config.js",
    ...
  } 

webpack.config.js:

const webpackConfig = require('@ionic/app-scripts/config/webpack.config');
const fs = require('fs');
const util = require('./scripts/build/util');

if (process.env.IONIC_ENV === 'prod') {
  webpackConfig.output.filename = '[name].[chunkhash].js';
  webpackConfig.plugins.push(
    new IndexFileUpdaterPlugin()
  );
}

function IndexFileUpdaterPlugin(options) { }

IndexFileUpdaterPlugin.prototype.apply = function(compiler) {
  compiler.plugin('done', function(stats) {
    let html = util.getIndexHtml();
    const fileType = 'js';
    html = util.appendOrOverwriteHashJsValues(html, 'main', util.getHashForFile('main', fileType));
    html = util.appendOrOverwriteHashJsValues(html, 'vendor', util.getHashForFile('vendor', fileType));
    util.writeIndexHtml(html);
  });
};

util.js:

const md5 = require('md5');
const fs = require('fs');


const util = module.exports = {}

const wwwDirPath = 'www';
const buildDirPath = 'www/build';

/* This could likely be pulled from Webpack stats, but this'll do for now ... */
util.getHashForFile = function(fileName, fileType) {
  const result = fs.readdirSync(buildDirPath).filter(file => {
    return file.startsWith(fileName) && file.endsWith(fileType) && (file.indexOf('map') === -1)
  })[0];
  return result.split('.')[1];
}

util.appendOrOverwriteHashJsValues = function(indexString, chunkName, hash) {
  let match = '(<script src="build/' + chunkName + '.*.js"></script>)';
  let regexp = indexString.match(match)[0];
  const replace = '<script src="build/' + chunkName + '.' + hash + '.js"></script>';
  return indexString.replace(regexp, replace);
}

util.appendOrOverwriteHashCssValues = function(indexString, chunkName, hash) {
  let match = '(<link href="build/' + chunkName + '.*.css" rel="stylesheet">)';
  let regexp = indexString.match(match)[0];
  const replace = '<link href="build/' + chunkName + '.' + hash + '.css" rel="stylesheet">';
  return indexString.replace(regexp, replace);
}

util.handleCssFileHashing = function() {
  const md5 = getMD5ForMainCss();
  fs.renameSync(buildDir('main.css'), buildDir('main.' + md5 + '.css'));
  fs.renameSync(buildDir('main.css.map'), buildDir('main.' + md5 + '.css.map'));
}

util.getIndexHtml = function() {
  return fs.readFileSync(wwwDir('index.html')).toString();
}

util.writeIndexHtml = function(htmlString) {
  fs.writeFileSync(wwwDir('index.html'), htmlString);
}

getMD5ForMainCss = function() {
  const fileString = fs.readFileSync(buildDir('main.css')).toString();
  return md5(fileString);
}

buildDir = function(file) {
  return buildDirPath + '/' + file;
}

wwwDir = function(file) {
  return wwwDirPath + '/' + file;
}

cacheBustCss.js:

const fs = require('fs');
const util = require('./util');

function doCssCacheBusting() {
  util.handleCssFileHashing();
  let html = util.getIndexHtml();
  const fileType = 'css';
  html = util.appendOrOverwriteHashCssValues(html, 'main', util.getHashForFile('main', fileType));
  util.writeIndexHtml(html);
}

doCssCacheBusting();

@nalmgren You saved my day. I've been looking around for the solution, but yours is perfect so far. Support lazy load as well.

Thanks !!!

This is what I'm using these days. I'm not using lazy loading though so I don't know whether or not it will support your needs in that case, but perhaps it could be extended to meet your needs.

https://gist.github.com/haydenbr/7df417a8678efc404c820c61b6ffdd24

@haydenbr
Thanks for sharing. Your solutions is great.
It's look like after ionic generate the result then only add the hashing into the file name.
This work without lazy load.

I have made small changes to the solution provided by @nalmgren , to work with Ionic 3.
Confirmed that it works with lazy loading. Here are all the steps:

  1. Created _your_app_root_/webpack.config.js
const webpackConfig = require('@ionic/app-scripts/config/webpack.config');
const fs = require('fs');
const util = require('./scripts/build/util');

if (process.env.IONIC_ENV === 'prod') {
  webpackConfig.prod.output.filename = '[name].[chunkhash].js';
  webpackConfig.prod.plugins.push(
    new IndexFileUpdaterPlugin()
  );
}

function IndexFileUpdaterPlugin(options) { }

IndexFileUpdaterPlugin.prototype.apply = function(compiler) {
  compiler.plugin('done', function(stats) {
    let html = util.getIndexHtml();
    const fileType = 'js';
    html = util.appendOrOverwriteHashJsValues(html, 'main', util.getHashForFile('main', fileType));
    html = util.appendOrOverwriteHashJsValues(html, 'vendor', util.getHashForFile('vendor', fileType));
    util.writeIndexHtml(html);
  });
};
  1. In you app root:
npm i md5 --save-dev
  1. Create _your_app_root_/scripts/build folder and add the following files:

util.js

const md5 = require('md5');
const fs = require('fs');


const util = module.exports = {}

const wwwDirPath = 'www';
const buildDirPath = 'www/build';

/* This could likely be pulled from Webpack stats, but this'll do for now ... */
util.getHashForFile = function(fileName, fileType) {
  const result = fs.readdirSync(buildDirPath).filter(file => {
    return file.startsWith(fileName) && file.endsWith(fileType) && (file.indexOf('map') === -1)
  })[0];
  return result.split('.')[1];
}

util.appendOrOverwriteHashJsValues = function(indexString, chunkName, hash) {
  let match = '(<script src="build/' + chunkName + '.*.js"></script>)';
  let regexp = indexString.match(match)[0];
  const replace = '<script src="build/' + chunkName + '.' + hash + '.js"></script>';
  return indexString.replace(regexp, replace);
}

util.appendOrOverwriteHashCssValues = function(indexString, chunkName, hash) {
  let match = '(<link href="build/' + chunkName + '.*.css" rel="stylesheet">)';
  let regexp = indexString.match(match)[0];
  const replace = '<link href="build/' + chunkName + '.' + hash + '.css" rel="stylesheet">';
  return indexString.replace(regexp, replace);
}

util.handleCssFileHashing = function() {
  const md5 = getMD5ForMainCss();
  fs.renameSync(buildDir('main.css'), buildDir('main.' + md5 + '.css'));
}

util.getIndexHtml = function() {
  return fs.readFileSync(wwwDir('index.html')).toString();
}

util.writeIndexHtml = function(htmlString) {
  fs.writeFileSync(wwwDir('index.html'), htmlString);
}

getMD5ForMainCss = function() {
  const fileString = fs.readFileSync(buildDir('main.css')).toString();
  return md5(fileString);
}

buildDir = function(file) {
  return buildDirPath + '/' + file;
}

wwwDir = function(file) {
  return wwwDirPath + '/' + file;
}

cacheBustCss.js

const fs = require('fs');
const util = require('./util');

function doCssCacheBusting() {
  util.handleCssFileHashing();
  let html = util.getIndexHtml();
  const fileType = 'css';
  html = util.appendOrOverwriteHashCssValues(html, 'main', util.getHashForFile('main', fileType));
  util.writeIndexHtml(html);
}

doCssCacheBusting();

Then to build my PWA for production, in my I execute those commands in my _app_root_:

npm run build --prod --base-href /new/ --build-optimizer
node scripts/build/cacheBustCss.js

That's it

I extended @haydenbr solution to work with lazy loading:

https://gist.github.com/meirmsn/9b37d6c500654b9a487e0c0a72583ef2

Notes:

  • This solution adds hashes to: main.js, main.css, polyfills.js, vendor.js, as well as the chunks generated for lazy loading
  • I had similar setup as @nalmgren and @acahinton: ionic app hosted on S3 behind AWS CloudFront
  • I don't think it works with service workers (haven't tried, but highly doubt it would work)

So yes, this adds yet one more to the list of solutions :s, so hopefully the Ionic team brings a standard one in the near future :)

@danbucholtz It's been a long time since this issue has closed.
Do Ionic Team still not having a plan to implement cache buster as a part of build process?
I'm waiting for this issue to be re-opened :)

I've just moved my ionic project to ngx-rocket

Leaning on Angular Cli's build system would solve many problems (including this issue)

Ionic 4 will use Angular CLI to do all the work.

Does anyone know if this works with the latest version of Ionic?

Was this page helpful?
0 / 5 - 0 ratings