Busting The Cache

Last week, I completed my new hexo theme, but newly added page were broken because the stylesheet were not updated with the page. This was caused by the cache either on browser side or on the CDN of github pages side. So I decided to solve this issue by using a method to busting the cache whenever I have updated the assets files.

If anyone have ever used yeoman to generate an angular project. You may find a grunt task in the generated Gruntfile called usemin. This task combined with several other task can concatenate your stylesheet and javascript files then minimize those files, at last a file revision task is executed internally. The key of busting cache is the file revision task which will calculate your files’ hash value and use the value to rename your file, this will totally solve the cache issue. whenever your file changes, the hash value changes and you get a different filename of assets.

Back to the hexo theme project, I found it is hard to use the grunt task directly in my project because those task will modify the html or template file to update the script and link tag in order to revision the assets file name. But hexo template file should only be modifed by programmer and generate html file by hexo. So I think it’s time to write a similar procedure by myself using hexo api.

Hexo provide two api helper and generator to generate tags in template and generate files with specified path. I will use this two api to keep the reference in template update and generate the new file with hash prefix file name. Because I can’t ensure the execution order of these two api, the concat and minified operation are abandoned. First let’s write a file hash method. This method accepts two arguments: file absolute path and hash digit length. because we don’t need the entire hash string to name asset file. the return value will be the new file name prefixed with a hash string

1
2
3
4
5
6
7
8
9
var fs = require('fs'),
path = require('path'),
crypto = require('crypto');
// modules above will be shared in the following two code blocks.
function filehash(filepath, digitlength) {
var hash = crypto.createHash('md5').update(fs.readFileSync(filepath)).digest('hex');
var prefix = hash.slice(0, digitlength);
return prefix + '.' + path.basename(filepath);
}

This method will be called by helper and generator to get the assets files’ hash value file name.

Now let’s begin from the generator. this api will let me to provide an array which tell hexo to write some files with specified path. I can copy the assets file with hashed file name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var themeSourcePath = path.resolve(__dirname, '../source'); // we need the theme source path to find the assets folder
var hashLength = 8;
// this is define of the generator.
hexo.extend.generator.register('filerev', function(locals) {
// find all directories in theme source directory.
var directories = fs.readdirSync(themeSourcePath);
var outputData = [];
directories.filter(function(dir){
return dir === 'css' || dir === 'js';
}).forEach(function(dir){
var files = fs.readdirSync(path.join(themeSourcePath, dir));
outputData = outputData.concat(files.map(function(file) {
return {
path: dir + '/' + filehash(path.join(themeSourcePath, dir, file), hashLength), // call filehash method we write before.
data: function() {
return fs.createReadStream(path.join(themeSourcePath, dir, file));
}
};
}));
});

return outputData;
});

This generator will generate a set of new file with the hash prefix and its original folder structure in public folder when you run hexo generate.

With the hash prefixed file generated properly, we have to update the reference in script and link tag. here we using helper to define an ejs helper called usemin to generate our script and link tag.

1
2
3
4
5
6
7
8
hexo.extend.helper.register('usemin', function(input) {
var filepath = path.join(themeSourcePath, input);
var dir = input.split('/');
var ext = path.extname(input);
var newFilename = filehash(filepath, hashLength);
var newPath = path.resolve('/', path.join(dir.slice(0, dir.length - 1).join('/'), newFilename));
return ext === '.js' ? '<script type="text/javascript" src="' + newPath + '"></script>' : '<link rel="stylesheet" href="' + newPath + '">';
});

Put this scripts file in scripts folder under my theme directory, the script will be loaded by hexo automatically. Now there are only one step to get the goal, modify template using the usemin helper

In head.ejs

1
2
3
4
5
<head>
<!-- other tags... -->
<%- usemin('css/styles.css') %>
<%- css('fancybox/source/jquery.fancybox.css') %>
</head>

In scripts.ejs

1
2
3
4
<!-- other tags... -->
<%- usemin('js/jquery.js') %>
<%- js('fancybox/source/jquery.fancybox.js') %> <!-- we can't process this file, so use the js helper -->
<%- usemin('js/caption.js') %>

When I run hexo generate, the index.html will be generated by hexo which contains head.ejs and scripts.ejs part

1
2
3
4
5
6
7
8
9
10
11
<head>
<!-- omit other tags... -->
<link rel="stylesheet" href="/css/2bb849c6.styles.css">
<link rel="stylesheet" href="/fancybox/source/jquery.fancybox.css">
</head>
<body>
<!-- omit other tags... -->
<script type="text/javascript" src="/js/cf26f8f0.jquery.js"></script>
<script src="/fancybox/source/jquery.fancybox.js" type="text/javascript"></script>
<script type="text/javascript" src="/js/33415e49.caption.js"></script>
</body>