Truly Multiple Entries with Webpack
29.06.2016, 18:16:15
In this post, I will try to explain how to handle situations when you need to use Webpack with a lot of small apps that share one codebase.
If you want to jump straight to the code, check out my repository on GitHub.
NOTE: Despite the fact that this happened to me already five times, I still consider this as a not typical case. Perhaps I was just lucky.
Webpack is your bro
After using Grunt and Gulp for building JavaScript apps for quite a while, I find them a little bit meh. If you ask me "why?" I won't be able to answer, though. Probably I don't have that good experience with them. Or I do…
I can talk a lot about how awesome Webpack is, but to do this I need a whole new post, so let's focus on it and its configuration.
Imagine that you have two main folders: core
and apps
; the apps
folder contains 50 micro SAPs. To make things easier, we'll call them widgets. Each widget is grouped by domain, thus one more level of nesting.
The folder structure may look like this, but it can be completely different:
~ src/
+ core/ < where all reusable things are
~ apps/ < microapps folder
~ weather/
~ temperature/
index.html
index.js < entry point
App.js
App.scss
ListComponent.js
ListComponent.scss
ListComponent.spec.js
+ humidity/
+ sunrise/
~ news/
+ feed/
+ sport/
And that is what we want to get:
~ dist/
~ weather/
~ temperature/
index.html
bundle.js
bundle.css
+ humidity/
+ sunrise/
+ news/
vendor.js
vendor.css
core.js
core.css
Bundling bundle
The very first thing we need for bundling is to get a list of entry points. You can find some documentation about multiple entry points in Webpack here.
In our case entry points are these multiple index.js
files, from which your application starts functioning. Webpack's multiple entry points object follows:
{
entry: {
%path_to_result_file_name%: %path_to_entry_point%,
// or
%path_to_result_file_name%: [
%path_to_entry_point_1%,
%path_to_entry_point_2%
]
}
}
Where path_to_result_file_name
is a path to the file we want to get as a result of bundling and path_to_entry_point
is a path to the file OR a module name which we want to bundle.
For example, assume we have the following configuration:
{
...
entry: {
'./one/cool/stuff/bundle.js': './src/another/nested/folder/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
},
...
}
The result of the webpack
will be:
./
~ dist
~ one
~ cool
~ stuff
bundle.js < bundled index.js and App.js
~ src
~ another
~ nested
~ folder
index.js
App.js < this file is requested from index.js
webpack.config.js
If you have a wtf-face, that's fine.
In our case [name]
was resolved as a ./one/cool/stuff/bundle.js
, the path relative to path.join(__dirname, 'dist')
. So webpack has written a file to path.join(__dirname, 'dist') + './one/cool/stuff/bundle.js'
, what will result in /dist/./one/cool/stuff/bundle.js
.
So, if we take our initial folder structure, then we can easily create the following entries:
{
...
entry: {
'./apps/weather/temperature/bundle.js': './src/apps/weather/temperature/index.js'
...
},
...
}
Looks good and we already know what will be the result of running this.
The bad part is that in this case, all the shared stuff from core
and all the modules will be duplicated in each bundle.js
. To solve this problem and leave these bundles as slim as possible, we should add one more entry point and utilize one plugin:
{
...
entry: {
'./apps/weather/temperature/bundle.js': './src/apps/weather/temperature/index.js',
...,
'vendor': [ 'react', 'moment', ... ]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.js'),
]
...
}
If we want to extract our core
, we can write another entry point with name core
and use CommonsChunkPlugin one more time. If you want to know more about this plugin, you can check here.
Now we're done with bundling things, but there is always "one more thing".
Hot Module Replacement and React Hot Loader
I will not cover the whole Webpack Dev Server, HMR, and React Hot Loader thing.
The only thing I'd like to tell here is what you need to do to get all that cool functionality working with a webpack config as we have made earlier.
To start listening to the HMR plugin you have two options: use --inline --hot
, kind of Live Reload thing, or add two more items to each entry point:
'./apps/weather/temperature/bundle.js': [
'./src/apps/weather/temperature/index.js',
'webpack/hot/only-dev-server',
'webpack-dev-server/client?http://localhost:8080'
]
Doing so enables HMR on each entry. But there is still one more thing to do. If you run Webpack Dev Server and try to change a file, you'll see, that HMR expects the changes chunk to be available from the current folder, e.g. http://localhost:8080/apps/weather/temperature/[hash].hot-update.json
, what will result in 404 error because by default Webpack does this:
this.set("output.hotUpdateChunkFilename", "[id].[hash].hot-update.js");
this.set("output.hotUpdateMainFilename", "[hash].hot-update.json");
So the needed chunk is available from root path, not from the current directory. The default values are fine when you're developing one app. But while we're dealing with multiple apps, we need to override these settings by understanding where this chunk is:
{
...
output: {
...,
hotUpdateMainFilename: '../../../[hash].hot-update.json',
hotUpdateChunkFilename: '../../../[id].[hash].hot-update.js'
}
...
}
You can check all default values here.
Conclusion
Don't forget to extract CSS.
Instead of a conclusion, I'd like to say a few good words about Webpack.
"Just a tool" is great when it does dull and predictable things well. "The Tool" is great when you can use it in uncommon cases.
The JavaScript tooling was always kind of "just a tool" for me, but somehow Webpack managed to become my "The Tool" in a quite short period.
Although the documentation is meh, I really encourage you to read the sources, so you'll get the whole picture of how it works and probably find more tricks.
If you know better ways of handling such cases, I'd love to see them in the comments. And don't hesitate to share the post if you find it useful.
P.S.
Why when it comes to more or less complex cases, each and every tutorial/boilerplate/template project is focused on just simple things?
One may say it's up to a developer to find out what to do with his own complicated case. I agree, but I find the fact, that you have close to none information about dealing with this kind of stuff, really disappointing.
I doubt that single page applications are the only thing that people do with JavaScript.
Argh…
Update
I made an example project on GitHub. Let me know if it's still unclear how to make things work.