Integrate Laravel with a Vue CLI app with Hot Module Replacement and no backend API
Tags: laravel, vue, inertiajs, spa
Published: 2021-05-26 23:26:12

Every time someone wants to use a [Vue.js](https://vuejs.org/) app generated by the [Vue CLI](https://cli.vuejs.org/) with [Laravel](https://laravel.com/), there's always that "YOU'RE GONNA NEED A BACKEND API" guy to crush your hopes and make you feel bad/sad. Well, there's nothing wrong with that approach. But every time I hear that, I always ask myself: WHY? Why do I need to use Laravel only as a backend API just because I'm going to use Vue CLI to serve my assets?

Sometimes it seems like people get stuck in a box of how things **MUST** be done and simply forget how the web works.

## Let's stop for a second...

Think about what the `npm run serve` from Vue CLI does.

It grabs some code, compiles it, **lifts up a simple web server and delivers the compiled assets**.

Did you notice something? it **LIFTS UP a server** and it **DELIVERS assets**. Isn't that what we do when we serve static assets from a CDN for our app? We have a server up, somewhere, that delivers static assets. That's all.

And **NO ONE, EVER** came down to you saying: *"Oh, your serving static assets, now you need a backend API."* I think you'll agree with me that this would be nonsense.

So, at the end, it seems that people forgot that when you do `npm run serve` and get Webpack DevServer running at `http://localhost:8080`, it's just like serving your assets from a CDN.

## Ok! I Get it, but...

Now I see, but there's this: Vue CLI is the one responsible for generating and serving **MY HTML PAGE with all the hashed `.js` and `.css` file references**. **THAT'S** why we serve the Vue CLI result upfront and use the Laravel app as a backend API, right?

Right, but what exactly is stopping us from using the HTML page generated by Vue CLI **as a view for Laravel to render** instead?

Let's start from there...

## Where should I create the Vue CLI app?

That's your choice, but the way I like to go about this is to make my resources folder the Vue app.

1. Backup your `resources` folder to `resources-bkp`.
2. Create the Vue app as a new `resources` folder
3. Place your `resources/lang` back to where it belongs
4. Create a new empty `resources/views` directory
5. Create a `resources/vue.config.js` file
6. If there's nothing important at `resources-bkp`, delete it

```bash
mv resources resources-bkp
vue create resources
mv resources-bkp/lang resources/lang
mkdir resources/views
nano resources/vue.config.js
rm -rf resources-bkp
```

> **NOTICE:** Keep in mind that, from now on, all of your `npm` commands must be run at the `resources` folder. I usually save some time by keeping 2 terminals open on my VSCode workspace: one at the project root and another at the `resources` folder. This way I can run commands at  the project root (php artisan, composer) without having to stop the DevServer running on a separate terminal.

## Make a base Blade template

Move your `resources/public/index.html` inside `resources/src/` and rename it to `template.blade.php`.

```bash
mv resources/public/index.html resources/src/template.blade.php
```

## Separating development and production builds

The HTML generated during development will be different from the one generated for production. For example, `link` and `script` tags will have distinct base URLs depending on the environment:

```html
<!-- development -->
<link href="http://localhost:8080/css/0.d09619e2.css" rel="prefetch">
<script type="text/javascript" src="http://localhost:8080/js/chunk-vendors.js"></script>

<!-- production -->
<link href="https://assets.mywebapp.com/css/0.d04519e3.css" rel="prefetch">
<script type="text/javascript" src="https://assets.mywebapp.com/js/chunk-vendors.a07692ee.js"></script>
```

So, we need to keep them separated. During development, we're going to put the generated views at `resources/views/devserver/`, while in production, at `resources/views/`.

We don't want to check in which environment we are every time  we need to render a view. So, instead of cluttering our code with `if` statements, we're gonna change our view `paths` config at `configs/view.php`.

This way, during development, Laravel will first lookup for views inside `resources/views/devserver/`, only if it can't find it there, it will look at `resources/views/`.

```php
// config/view.php

    'paths' => (env('APP_ENV', 'production') === 'local')
        ? [
            resource_path('views/devserver'),
            resource_path('views')
        ]
        : [
            resource_path('views')
        ],
```

We also don't want to commit our development views to our repository, so, add the path to your `.gitignore` file.

```bash
# .gitignore
/resources/views/devserver
```

Regarding our assets, we're gonna dump them at `public/dist/`. During development, however, they are going to be served directly from memory by Webpack DevServer.

## Configuring Vue CLI properly

Here's the Vue CLI config we'll need. I commented it in details.

```javascript
const path = require('path')

let environmentViewsDirectory = ''
let outputDir = 'dist/'

/**
 * This is a precaution
 * In case we accidentally write dev-server files to disk,
 * we're not going to mess with our production ready dist folder
 */
if (process.env.NODE_ENV === 'development') {
  environmentViewsDirectory = 'devserver/'
  outputDir = 'dist-devserver/'
}

/**
 * A helper function to create Vue CLI page entries
 * @param {string} name The name of the JavaScript entry point for the page
 * @param {string} outputPath The path to the resulting HTML Blade file without the extension
 * @param {string} template An optional template file to be used as base for the Blade view
 * @returns 
 */
const page = (name, outputPath, template) => {
  return {
    [name]: {
      entry: `src/${name}.js`,
      filename: path.resolve(__dirname, `views/${environmentViewsDirectory}${outputPath}.blade.php`),
      template: template || 'src/template.blade.php'
    }
  }
}

module.exports = {
  /**
   * If you're not aiming for a SPA
   * You can add as many pages here as you want
   * as long as you create an entry .js file at resources/src/
   * You can also use this to create a separate template for an admin endpoint or any other endpoint you need
   */
  pages: {
    ...page('main', 'app'),
    // ...page('contact', 'contact'),
    // ...page('admin', `admin`),
  },
  // Where to dump resulting files
  outputDir: `../public/${outputDir}`,
  // The URL from which assets will be served
  // AND WHICH will be used by injected assets inside the HTML
  publicPath: process.env.VUE_APP_ASSET_URL,
  css: {
    extract: (process.env.NODE_ENV === 'development')
      ? {
        filename: 'css/[name].css'
      }
      : true
  },
  devServer: {
    // Public needs to be explicitly set since we're not using the defaults
    public: process.env.VUE_APP_ASSET_URL,
    // During development, we want to write ONLY the HTML files to the disk
    // This is needed because Laravel won't be able to read the view directly from memory
    // But we want our .js and .css assets to still be served directly from memory
    // So, we're writing to disk ONLY if the output file is at views/devserver/
    writeToDisk: (filePath) => {
      return /views\/devserver\//.test(filePath);
    },
    // Allows you to point any host to the devserver port.
    // Eg.: http://assets.localhost:8080 will also work
    // Alternatively you could use the "allowedHosts" option
    disableHostCheck: true,
    // Avoid CORS issues
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
      "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
    }
  }
}

```

## IMPORTANT: process.env.VUE_APP_ASSET_URL

The `process.env.VUE_APP_ASSET_URL` will be set at `resources/.env.local` and `resources/.env.production`.

At `resources/.env.local`,  `VUE_APP_ASSET_URL` must be your DevServer host (don't forget the trailing slash):

Create the files.

```bash
nano resources/.env.local
nano resources/.env.production
```

```bash
# resources/.env.local
VUE_APP_ASSET_URL=http://localhost:8080/
```

At `resources/.env.production` it depends. Usually, in production, I setup a subdomain with its root pointing to `public/dist/`. This way I can easily cache all of its contents.

In this case, `VUE_APP_ASSET_URL` will simply be the subdomain. Otherwise, we'll have to add `dist/` at the end of the address.

```bash
# resources/.env.production

# subdomain pointing to public/dist/
VUE_APP_ASSET_URL=https://assets.mywebapp.com/

# same domain
VUE_APP_ASSET_URL=http://www.mywebapp.com/dist/
```

Now you can run `npm run serve` from the `resources` folder.

```bash
cd resources
npm run serve
```

## Routing

Now, there's two ways you can go about routing:

### Method 1

Generate a separate view file for each page (see the comment on the `resources/vue.config.js` `pages` property). This means that every time you create a new page, you'll need to add a new entry `.js` file at `resources/src/` (Eg.: `resources/src/mypageentry.js`) and add it to that object, like mentioned in that comment (`...page('mypageentry', 'mybladefile')`). In this case, you simply return views like you always did with Laravel.

```php
// routes/web.php

Route::get('/', function () {
    return view('app');
});

// Route::get('/contact', function () {
//    return view('mybladefile');
// });
```

### Method 2 (recommended)

Use the same view for every request and decide which component to load on the client side based on a page key. I prefer this approach, but it'll require some work to get Code Splitting working. This is to avoid loading components that are not required on the current page.

Create a dedicated folder for your page components.

```bash
mkdir resources/src/pages
```

Create a `Home.vue` page.

```html
// resources/src/pages/Home.vue

<template>
  <div class="home">
    <h1>Hello from Home component</h1>
  </div>
</template>
```

Change your `App.vue`.

```html
// resources/src/App.vue

<template>
  <div id="app">
    <Page />
  </div>
</template>

<script>
export default {
  name: 'App',
  components: {
    Page: () => import('@/pages/' + window.app.page.component + '.vue')
  }
}
</script>
```

As you can see, we're now getting which page should be rendered through a global object attached to the `window` object. To get this in place, add the following to your `resources/src/template.blade.php`:

```html
<head>
  <!-- ... -->
  <script>
    window.app = @json($jsapp)
  </script>  
</head>
```

Then, pass the data through your route:

```php
// routes/web.php

Route::get('/', function () {
    return view('app', [
        'jsapp' => [
            'page' => [
                'component' => 'Home'
            ]
        ]
    ]);
});
```

## This is a bit bitter... A little sugar, please?

Off course we're not gonna be passing all this array every time we render a view. We're not troglodytes!

Let's create a View Macro to simplify this task.

```php
// app/Providers/AppServiceProvider.php

public function boot()
{
    View::macro('vue', function ($component, $data = []) {
        return view('app', [
            'jsapp' => [
                'page' => [
                    'component' => $component,
                    'data' => $data
                ]
            ]
        ]);
    });
}
```

Then we can just do this:

```php
// routes/web.php

Route::get('/', function () {
    return View::vue('Home'); // Cool, huh?
});
```

## But... how do I pass server data to Vue components?

The server side part is done. Our `View::vue` macro accepts an array of data as a second parameter.

Let's make that data easily accessible to all of our components using a global mixin (if you have a better way of doing this, go ahead).

```javascript
// resources/src/main.js

Vue.mixin({
    data () {
      return {
        page: window.app.page.data
      }
    }
})

new Vue({
  render: h => h(App)
}).$mount('#app')
```

Now, pass some data from your route:

```php
// routes/web.php

Route::get('/', function () {
    return View::vue('Home', [
        'title' => 'Imaginations from the server side!!'
    ]);
});
```

Now we can simply use that title in our components:

```html
// resources/scr/components/Home.vue

<template>
  <div class="home">
    <h1>{{ page.title }}</h1>
  </div>
</template>
```

Isn't that awesome? We have transcended the limits between the PHP server and the Vue.js components world!

## What about SPAs?

Well, the second routing method we saw is the mechanism used by [Inertia.js](https://inertiajs.com/), an awesome way of creating SPAs with Laravel and Vue.js. **[You can learn how to integrate Inertia.js with Vue CLI here](https://dev.to/mtdalpizzol/integrate-laravel-inertia-js-with-a-vue-cli-app-32ac)**. Spoiler alert: if you followed this article, you already did 90% of the work and all you did here will be used there. In fact, everything from the *Routing* section of this article won't be needed. I **HIGHLY** recommend you to take that step further. Inertia.js is awesome.

## Article repo

I've made a [GitHub repo with all of this work done](https://github.com/MtDalPizzol/laravel-vue-cli) for you to clone and see things working for yourself.

> **DON'T FORGET TO CREATE YOUR `resources/.env.local` FILE AND SET THE VUE_APP_ASSET_URL ENVIRONMENT VARIABLE**

## Conclusion

As we saw, using Laravel as a simple API backend for your Vue CLI app isn't the only way to get an app with a Vue.js based frontend. You can use the full routing power and other awesome features of Laravel like you always did and just use Vue.js for what it was initially intended: working with the view layer of your app. And, if you're all about SPAs, you can take one more step and **[integrate Inertia.js with a Vue CLI app](https://dev.to/mtdalpizzol/integrate-laravel-inertia-js-with-a-vue-cli-app-32ac)**.

Hope this helps anyone out there. Cheers!