Table of contents
- Foreword
- Folder structure
- Cachebusting
- One last thing about javascript
- Config file
- How to use
- Conclusion (and source on github)
Foreword
If you're developer/designer who wants to focus only on building the product, use the latest and greatest technology stack and don't spend much time on configuration this article is for you. This is about webpack - a big and very powerful monster that can take a lot of your time away if you're not familiar with its concepts. To be honest, it's very difficult to grasp at first, since webpack itself is just a bundler and more often is used in conjunction with other plugins inside nodejs script. However, the goal of this article is not to overwhelm you but provide a production-ready configuration that you can "copy-paste", change few lines of code with your flavor and build the next awesome thing.
Before we move on, I need to mention that this webpack setup is suitable only for specific types of projects and technologies. In other words, if you:
- Build MPA (multi page application, powered by CMS like WordPress, static site generators like Jekyll, Hexo or any other custom "back-end first" engine)
- Use ES6 and later versions of javascript with an ability to transpile down to ES5
- Use SCSS
- Want "live-reload" your browser on any JS or CSS/SCSS change compatible with your Back-end solution
- Need development/production environment set up (where in development all sourcemaps are available and the code isn't minified as in production it's all obfuscated, minified and javascript/css file names have "hash" in their names to bust the client's browser cache)
This config will definitely help you. However, if you:
- Build SPA (single page application)
- Need to use TypeScript, JSX or something different from regular Javascript
- Need to bundle images, htmls
- Need to have html-hot-reload
This config won't help you (although you can get some ideas from it).
Folder structure
There're some things that are very similar for each MPA: the folder structure for the web part. It might be hidden very deeply behind the back-end code or stay very close to the project root, but basically, it looks like this:
|-- src
|-- js
whatever.js
main.js
|-- scss
whatever.scss
style.scss
I completely understand, that example above is an extremely simplified version of real production code and location of javascript/scss files might be different, but the idea is there must be an entry point for each of them somewhere: main.js (or index.js or entry.js, etc.) and style.scss (or main.scss, etc.). That what will be specified in the webpack configuration.
The output folder for compiled resources will be called
dist
(you can adjust it in the way you like). Taking that
into account, the final folder structure will turn into this:
|-- dist // generated javascript and css.
|-- src // source code for ES6 and SCSS
|-- js
whatever.js
main.js
|-- scss
whatever.scss
style.scss
Cachebusting
As I mentioned before, one of the "must to have" features for
production is a cachebusting (if you're unfamiliar with the concept this
article might help). Here's a logic how it's going to work: Every time
when webpack will go through the build process it will write a json file
with random string(hash) in there. There is an internal mechanism inside
the webpack that will take care of a hash generation. The end file will
have a name cachebuster.json
(you can pick any name you
prefer) and contain the following info:
{
"resourcesHash":"3b94fe477630f0672c42"
}
Later on, back-end engine will parse this json file and insert a hash
(in this case 3b94fe477630f0672c42
) into css/js paths.
One last thing about javascript
In this example, I used babel to
compile new javascript into ES5. In order to successfully use the
configuration provided in this article you need to create
.babelrc
file (at the same level as webpack configuration
file) with the following content:
{
"presets": ["es2015"]
}
Config file
var webpack = require('webpack');
var path = require('path');
var fs = require('fs');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var LiveReloadPlugin = require('webpack-livereload-plugin');
var inProduction = process.env.NODE_ENV === 'production';
var CASHBUSTER_FILE = './cachebuster.json';
// Function that extracts unique hash generated by webpack
// and writes it into a file. It also performs a check
// whether cachebuster.json existed before and if so,
// replaces the hash value. Otherwise writes a new file.
function generateResourcesHash(hash) {
if (!fs.existsSync(CASHBUSTER_FILE)) {
fs.openSync(CASHBUSTER_FILE, 'w');
fs.writeFileSync(
path.join(__dirname, "", CASHBUSTER_FILE),
"{}"
);
}
var cacheBuster = JSON.parse(fs.readFileSync(path.join(__dirname, CASHBUSTER_FILE), "utf8"));
cacheBuster.resourcesHash = hash
fs.writeFileSync(
path.join(__dirname, "", CASHBUSTER_FILE),
JSON.stringify(cacheBuster)
);
}
// Entry point in the webpack configuration
module.exports = {
devtool: inProduction ? '' : 'source-map',
entry: {
main: [
'./src/js/main.js', // Javascript entry point
'./src/scss/style.scss' // SCSS entry point
]
},
output: {
path: path.resolve(__dirname, './dist'), // output folder is named as dist
filename: inProduction ? 'main.bundle.[hash].js' : 'main.bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.s[ac]ss$/,
use: ExtractTextPlugin.extract({
use: [{
loader: 'css-loader',
options: {
sourceMap: inProduction ? false : true,
minimize: inProduction ? true : false
}
}, {
loader: 'sass-loader',
options: {
sourceMap: inProduction ? false : true
}
}],
publicPath: '/dist'
})
},
]
},
plugins: []
}
// Environment specific config for plugins and
// hash value update.
if (inProduction) {
module.exports.plugins.push(
new webpack.optimize.UglifyJsPlugin(),
function() {
this.plugin("done", function(statsData) {
var stats = statsData.toJson();
if (!stats.errors.length) {
generateResourcesHash(stats.hash);
}
});
},
new ExtractTextPlugin({
filename: function (getPath) {
var hash = getPath('[hash]');
generateResourcesHash(hash);
return getPath('style.[hash].css');
}
})
)
} else {
module.exports.plugins.push(
new ExtractTextPlugin('style.css'),
new LiveReloadPlugin(),
)
}
How to use
Run
webpack
for development mode (one-time build).Run
webpack --watch
for development watch mode (will watch auto-recompile on every scss/js change).Run
NODE_ENV=production webpack
for production build.To insert hash in production, add this logic to your project (this is pseudo code to give you an idea):
if ('production') { <link rel="stylesheet" type="text/css" href="/dist/style.{{resourcesHash}}.css"> } else { <link rel="stylesheet" type="text/css" href="/dist/style.css"> }
Install chrome plugin live-reload to support live-reload on each js/css change
Conclusion
Dealing with environment set up is not easy, especially when the
project has custom requirements. However, sometimes it's even hard to
start with basics when dealing with webpack. This article won't solve
configuration problem for you codebase, but it might help you with the
general idea which way to go. You can also find source code on
github
with all dependencies listed in package.json
and start
using it. Happy coding.