As I spend more and more time in the Vue.js ecosystem, I’m developing an a much greater appreciation for the time the core team has spent developing tools that make managing the project configuration.
That said, a big part of managing a Vue.js project still requires a lot of Webpack knowledge… More than I have, unfortunately.
Today I was working on a sort of micro-application that would be embedded on various web pages that I really have no control over. More specifically, it’s a web app that enables consent management for the CCPA privacy regulations, and it handles conditionally popping open a small UI for California visitors and then providing the opt-out signals to upstream code like prebid.js.
This app needs to load on all sorts of third-party web pages, and ideally the functionality would be enabled by simply including a JavaScript in the header of the page before the rest of the site’s advertising technology stack would load. This means the Vue.js app doesn’t really own the page, and it really needed to load as a cleanly as possible in a single JavaScript file.
This took a little more wrangling that I’d expected, and much like my deeper dives into doing things like adding Snap.svg to Vue and Nuxt projects, it required a bit of a deeper dive into Webpack and the configuration chaining behavior of the newer vue-cli generated apps.
Injecting your own mount DIV
One of the things your Vue.js project always needs is a place to mount itself on the web page. Generally, your project will have some sort of HTML template that hosts it, and often your whole site may be the actual Vue.js app and that simple HTML file is all you need to deploy.
More often than not, however, I’m loading Vue.js apps onto existing pages, and that involves just adding a <div> element somewhere with the right ID in an existing layout.
For this project, I had zero control over the host pages so I had to dynamically inject a mount point. To do this, I had to modify the main.js file in my Vue project to do something like this…
import Vue from 'vue' import App from './App.vue' let gMyApp = undefined; document.addEventListener("DOMContentLoaded", () => { // Inject a private mount point div to display UI... let body = document.getElementsByTagName('body')[0]; body.innerHTML = body.innerHTML + "<div id=myApp_mount />"; gMyApp = new Vue( { render: h => h(App), } ).$mount('#myApp_mount') } );
Inspecting the generated webpack configuration redux
In my previous article, I talked about inspecting the vue-cli generated Webpack configuration by using ‘vue inspect’ to eject a copy of the configuration, and then modifying it using webpack-chain in the vue.config.js file.
One detail that I missed in this earlier exploration was the need to eject a production configuration. To do this, you use the following command and your shell…
vue inspect --mode production > ejected.js
Without the mode command line variable, what you’re seeing is your development configuration, which can run a whole different chain of loaders and perform other Webpack mischief you’re not expecting.
vendors.js must go!
But back to my original problem of getting a single bundle as output from the project. By default, the Vue.js CLI will kick out separate bundles for your code, vendor libraries and any extracted CSS. Trying to hand this off to a third party and explaining they needed to include a dog’s breakfast of separate files on all of their site pages was a deal breaker, so figuring out how to merge all of these files in the production build was important.
The easy first step was to just stop Webpack from splitting the JavaScript into separate chunks. That’s actually Webpack’s default behavior, so all we need to do is get rid of some of the configuration passed into the split chunks plugin by vue-cli’s defaults. Using chain-webpack in vue.config.js, that looks something like this…
module.exports = { chainWebpack: config=> { // Disable splitChunks plugin, all the code goes into one bundle. config.optimization .splitChunks( ).clear(); } }
Combining CSS and JavaScript in one Webpack bundle
That got the project down to a single emitted JavaScript file and a single CSS bundle. And that’s where things got interesting.
To get the CSS to reside with the JavaScript code, it needed to be packaged and injected. That mean undoing the extraction of the CSS into its separate file and then separately using style-loader to sneak it into the DOM when the JavaScript was executed on the page.
The first step was disabling the existing extraction process handled by the extract-css-loader. Again, inside vue.config.js in the chainWebpack:config function…
config.module.rule('css').oneOf('vue').uses.delete('extract-css-loader');
config.module .rule('css') .oneOf('vue') .use('style-loader') .before('css-loader') .loader('style-loader') .end();
With that in place, the project kicked out a single .js file that had everything in it… vendor and project code, plus the css. And, the css actually got injected when the script ran. Perfect!
Bonus Episode! Renaming the Webpack Output File
// Get the filename into the format we use for different builds. let today = newDate(); let mm = today.getMonth() +1; // getMonth() is zero-based let dd = today.getDate(); let yy = today.getFullYear().toString().substr(-2); let dateStr = [ yy, (mm>9?'':'0') + mm, (dd>9?'':'0') + dd ].join(''); config.output.filename( 'myapp-'+ dateStr +'.js' )
Perhaps a little wordy, but it gets the job done.
Putting it all together in one webpack chain configuration
module.exports = { chainWebpack: config=> { // Get the filename into the format we use for different builds. let today = newDate(); let mm = today.getMonth() +1; // getMonth() is zero-based let dd = today.getDate(); let yy = today.getFullYear().toString().substr(-2); let dateStr = [ yy, (mm>9?'':'0') + mm, (dd>9?'':'0') + dd ].join(''); config.output.filename( 'myapp-'+ dateStr +'.js' ) // Disable splitChunks plugin, all the code goes into one bundle. config.optimization.splitChunks( ).clear(); // Disable the CSS extraction into a separate file. config.module.rule('css').oneOf('vue').uses.delete('extract-css-loader'); // Take the CSS from the bundle and inject it in the DOM when // the page loads... config.module .rule('css') .oneOf('vue') .use('style-loader') .before('css-loader') .loader('style-loader') .end(); } }
config.module.rule('css').oneOf('vue')...
Thanks for the article, it did work for me for vendor and css, but for other chunks didn’t work. Although I learned a lot with this article which helped me to understand better the configuration in vue.
In the end I didn’t need the :
config.optimization.splitChunks( ).clear();
I just need to setup maxChunks to 1 in vue.config.js
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
]
Thanks again for your article.