Introduction

Laravel is a modern open-source PHP framework for web applications, created by Tailor Otwell. It has been designed to produce elegant code, run fast and simplify web development. It comes with a lot of tools such as a router, a queue manager (Horizon), a CLI (Artisan), and a front builder (Mix).

Laravel Mix is a tool providing front assets compilation with various preprocessor for JS and CSS. Mix is based on the great Webpack builder and provides a higher level API to simplify pre-processing configuration.

Unfortunately, Laravel Mix has its own limits: it doesn’t provide a great flexibility when dealing with multiple frontend application hosted in the same projet; mix cannot generate multiple vendors, split assets easily to different folders. Hopefully, I have found an elegant solution to fix this issue and I hope it will help you!

Follow the guide!

Trying to split dependencies into different vendors using Laravel Mix

Let’s say you have a Laravel project with two frontend applications:

  • A back-office application using bootstrap and jQuery and to perform AJAX request on API endpoints ;
  • A customer-facing application powered by VueJS to provide a more advanced UX.

Why would I want to split vendors?

These two front applications require totally different Javascript packages. Additionally, they may share some dependencies. For performances reasons, we want to separate these package into two different vendors that will be loaded separately by each application; you don’t want your VueJS application to load bootstrap and jQuery for nothing.

Unfortunately, if you are using Laravel Mix (and you should as it simplify building process!), you won’t be able to build two separate vendors for your two front applications out of the box.

[Optional] Instantiate a Laravel project with two front applications

This whole section is optional. I want to provide a specific and real context before describing the solution and discussing performances improvements. Plus, all sources described here are available on Luckey Homes GitHub public repository. Hope it will give you a better understanding of our problem!

Project setup

First step, install Laravel installer globally on your computer:

composer global require laravel/installer

You may need to add ext-zip PHP extension to successfully install latest version.

Once done, run Laravel CLI:

laravel new laravel-mix-multiple-vendors
cp .env.example .env
php artisan key:generate

Finally, start your server by typing:

php -S localhost:8080 -t ./public

Two front applications: backoffice and customers

We are going to modify this basic Laravel setup to create two different front applications. I won’t focus on CSS in this guide. First, create two directory in ressources/js folder:

mkdir ressources/js/backoffice && mkdir ressources/js/customers

Then:

mv ressources/js/bootstrap.js ressources/js/backoffice/bootstrap.js

rm ressources/js/app.js
rm ressources/js/components/ExampleComponent.vue
rm ressources/views/welcome.blade.php

In backoffice folder, create a backoffice.js file. This file is our back-office JS entry point. Let’s use it to make sure jQuery is running:

require('./bootstrap');
 
$(".hello-world-container-jquery" ).html("Hello Word with jQuery")

In customers folder, create a customers.js (our customer application entry point) file and a HelloWorld.vue component:

import Vue from 'vue'
 
Vue.component('hello-world', require('./HelloWorld.vue'));
 
const app = new Vue({
 el: '#app'
});
<template>
   <div class="container">
       Hello World with vue
   </div>
</template>
 
<script>
   export default {
       mounted() {
           console.log('Hello World')
       }
   }
</script>

Laravel project created through Laravel CLI already comes with jquery, bootstrap, and VueJS. In order to install JS dependencies, run:

yarn install

Create two routes in your Laravel routes/web.php: (and you can delete default welcome route by the way)

<?php
/* ... */
Route::get('/backoffice', function () {
   return view('backoffice');
});
Route::get('/customers', function () {
   return view('customers');
});

Create corresponding views in ressources/views folder and make sure these views are loading corresponding JS entry points. These JS entry points are loaded through mix() helper that looks into your public/mix-manifest.json folder and generate link to public/js folder once built by Laravel Mix.

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
   <head>
       <meta charset="utf-8">
       <meta name="viewport" content="width=device-width, initial-scale=1">
 
       <title>Backoffice</title>
   </head>
   <body>
   <div class="flex-center position-ref full-height">
       <div class="content">
           <h1 class="title m-b-md">
               Backoffice
           </h1>
           <div class="hello-world-container-jquery">
               Page is loading...
           </div>
       </div>
   </div>
   <script src="{{ mix('js/backoffice/backoffice.js') }}"></script>
   </body>
</html>

(ressources/views/backoffice.blade.php)

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
   <head>
       <meta charset="utf-8">
       <meta name="viewport" content="width=device-width, initial-scale=1">
 
       <title>Customers</title>
   </head>
   <body>
   <div class="flex-center position-ref full-height">
       <div class="content">
           <h1 class="title m-b-md">
               Customers
           </h1>
           <div id="app">
               <hello-world></hello-world>
           </div>
       </div>
   </div>
   <script src="{{ mix('js/customers/customers.js') }}"></script>
   </body>
</html>

(ressources/views/customers.blade.php)

At this point, your ressources folder should look like this:

ressources folder with two separate front applications

Basic build configuration

Update laravel mix config (webpack.mix.js):

const mix = require('laravel-mix');
 
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
 
mix.js('resources/js/backoffice/backoffice.js', 'public/js/backoffice')
mix.js('resources/js/customers/customers.js', 'public/js/customers')

And then build your assets using:

npm run dev

Here is what your public folder should look like:

Browsing our applications

Open a browser and go to http://localhost:8080/customers to view Customers application. (I agree it’s ugly!). If you can see ‘Hello World’, that means that jQuery is loaded. In your Network tab (developer tools), you will see that customer.js is loaded ~300KB.

First glance at our beautiful customers application

Having a look to Back-Office application: Vue seems to be loaded and backoffice.js weights ~1.0MB:

Backoffice application

That’s it! At this step, we are not using vendors at all. customer.js and backoffice.js have been built by Laravel Mix (and Webpack) and contains exactly what they need. But that’s not perfect for many reasons:

  • as applications get more complex, we want to separate pages script from packages dependencies in order to optimize performances: a heavy vendor containing dependencies is loaded once and then cached by the browser while pages scripts are loaded depending on the page user is on ; Using Webpack, you can dynamically load your assets from your vendor, only when they are needed. This method significantly improve performances ;
  • to improve User Experience, developers try to provide a first print of the page as soon as possible. That’s why it’s good to separate must-have JS scripts from secondary/heavy scripts that can be loaded later into different files.

Trying to build multiple vendors

Let’s try to go one step further and set up vendors. Based on this official documentation, we can update our webpack.mix.js script in the following way:

const mix = require('laravel-mix');
 
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
 
mix.js('resources/js/backoffice/backoffice.js', 'public/js/backoffice')
 .extract(['jquery', 'bootstrap', 'lodash', 'popper.js', 'vue'])
mix.js('resources/js/customers/customers.js', 'public/js/customers')

Run npm run dev again. You should get the following output:

We note that even if we add .extract() instruction on backoffice.js, backoffice vendor.js file is attached to customers folder. And if you try to split your dependencies into many vendors using multiple extract() instruction, you won’t be able to build your assets in multiple vendors:

const mix = require('laravel-mix');
 
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
 
mix.js('resources/js/backoffice/backoffice.js', 'public/js/backoffice')
 .extract(['jquery', 'bootstrap', 'lodash', 'popper.js'])
mix.js('resources/js/customers/customers.js', 'public/js/customers')
 .extract(['vue'])

Nice try!

At this point, you might be tempted to use this vendor.js in your two applications by adding the following lines to your two views template:

<script src="{{ mix('js/customers/manifest.js') }}"></script>
<script src="{{ mix('js/customers/vendor.js') }}"></script>

If you refresh your applications, you will get:

Vendor.js size is 1.3MB (makes sense!). It’s very heavy compared to 300KB we had before!

Vendors.js is loaded here too. At least, it will be cached by user’s browser!

How can we build multiple vendors using Laravel Mix?

One solution: add option to your npm scripts

It may not be the only solution, but this is the only one I could get working! Here is our plan:

  • Write different Laravel Mix configuration file for each front application we have ;
  • During building process, build all different Laravel mix configurations separately ;
  • Make sure Laravel is able to mix between built folders!

Split your build into multiple mix files

Start by creating two different mix files: webpack.backoffice.mix.js and webpack.customers.mix.js. These two files are using their own entry point and are defining their own vendor:

const mix = require('laravel-mix');
require('laravel-mix-merge-manifest')
 
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
 
mix.js('resources/js/backoffice/backoffice.js', 'public/js/backoffice')
 .extract(['jquery', 'bootstrap', 'lodash', 'popper.js'])
 .mergeManifest()
const mix = require('laravel-mix');
require('laravel-mix-merge-manifest')
 
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
 
mix.js('resources/js/customers/customers.js', 'public/js/customers')
 .extract(['vue'])
 .mergeManifest()

Do not take care of .mergeManifest() for now! It’s explained below.

Instead of running npm run dev, let’s specify to our npm script what application should be built. This can be achieved by using npm scripts options (npm –section=backoffice run dev or npm –section=customers run dev) and updating the following main webpack.mix.js script, so it redirects to our specific webpack.

Our new webpack.mix.js:

if (['customers', 'backoffice'].includes(process.env.npm_config_section)) {
 require(`${__dirname}/webpack.${process.env.npm_config_section}.mix.js`)
} else {
 console.log(
   '\x1b[41m%s\x1b[0m',
   'Provide correct --section argument to build command: customers, backoffice'
 )
 throw new Error('Provide correct --section argument to build command!')
}

Options passed to npm are available in scripts using process.env.npm_config_{option}.

If you try to build your assets now, you will face one last problem. When writing links to your assets from laravel templates, you usually want to use {{ mix() }} helper. To resolve dependencies, mix() helper takes a look at public/mix-manifest.json file. Unfortunately, while building your application in multiple steps (first backoffice, then customers.js), this mix-manifest.json will be overwritten.

To fix it, you can use laravel-mix-merge-manifest package:

yarn add laravel-mix-merge-manifest

And add .mergeManifest() instruction to your webpack.{app}.mix.js files (see above).

Finally, run:

npm --section=backoffice run dev && npm --section=customers run dev

Successful output:

Have a look to your mix-manifest.json and update mix path in your blade templates to use dedicated vendors:

{
   "/js/backoffice/backoffice.js": "/js/backoffice/backoffice.js",
   "/js/backoffice/vendor.js": "/js/backoffice/vendor.js",
   "/js/backoffice/manifest.js": "/js/backoffice/manifest.js",
   "/js/customers/vendor.js": "/js/customers/vendor.js",
   "/js/customers/customers.js": "/js/customers/customers.js",
   "/js/customers/manifest.js": "/js/customers/manifest.js"
}
<script src="{{ mix('js/customers/manifest.js') }}"></script>
<script src="{{ mix('js/customers/vendor.js') }}"></script>
<script src="{{ mix('js/customers/customers.js') }}"></script>
 
// and
 
<script src="{{ mix('js/backoffice/manifest.js') }}"></script>
<script src="{{ mix('js/backoffice/vendor.js') }}"></script>
<script src="{{ mix('js/backoffice/backoffice.js') }}"></script>

Browse your two applications again and… TADAA!

Backoffice application
Customers application

We now have very light customers.js and backoffice.js file and dedicated vendors (300KB + 999KB). Perfect!

Additional tips

Here is one protip! In order to run both build in one command concurrently:

yarn add concurrently

And update your package.json script section:

"scripts": {
       "dev-all": "concurrently \"npm --section=backoffice run dev\" \"npm --section=customers run dev\"  --kill-others-on-fail",
       "dev": "npm run development",
       "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
       "watch": "npm run development -- --watch",
       "watch-poll": "npm run watch -- --watch-poll",
       "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
       "prod": "npm run production",
       "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
   },

And then:

npm run dev-all

Conclusion

Laravel mix is powerful but has its own limit. Understanding built process from npm scripts to mix() helper helped me a lot while designing this workaround.

I’ve been struggling a few hours with this problem, without finding any proper solution on the Internet. I want to share my own solution with you and I hope this will help some of you building robust Laravel applications!

All sources described here are available on a GitHub public repository.