Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Avoid This Common Anti-Pattern in Full-Stack Vue/Laravel Apps

DZone's Guide to

Avoid This Common Anti-Pattern in Full-Stack Vue/Laravel Apps

In this post, we'll look at a design pattern that makes it easy to inject initial application state into the head of the HTML page, and allows for a lot of flexibility.

· Web Dev Zone
Free Resource

Get deep insight into Node.js applications with real-time metrics, CPU profiling, and heap snapshots with N|Solid from NodeSource. Learn more.

If you want your Vue.js single-page app to communicate with a Laravel backend, you will, quite reasonably, think of using AJAX. Indeed, Laravel comes with the Axios library loaded in by default.

However, it's not advisable to use AJAX to retrieve application state on the initial page load, as it requires an extra round-trip to the server that will delay your Vue app from rendering.

I see many full-stack Vue/Laravel apps architected in this way. An alternative to this anti-pattern is to inject initial application state into the head of the HTML page so it's available to the app as soon as it's needed. AJAX can then be used more appropriately for subsequent data fetches.

Using this approach can get messy, though, if your app has different routes requiring different initial state. In this article, I'll demonstrate a design pattern that makes it very simple to implement this injection approach, and allows for a lot of flexibility, even in multi-route apps.

As you'll shortly see, an example app I created is interactive 25% sooner when implementing this design pattern.

Note: this article was originally posted here on the Vue.js Developers blog on 2017/08/06

Passing Data to Vue From Laravel

Here's an example full-stack Vue/Laravel app I built for Oldtime Cars, a fictitious vintage car retailer. The app has a front page, which shows available cars, and a generic detail page, which shows the specifics of a particular model.

Image title

This app uses Vue Router to handle page navigation. Each page needs data from the backend (e.g. the name of the car model, the price, etc.), so a mechanism for sending it between Vue and Laravel is required. The standard design pattern is to setup API endpoints for each page in Laravel, then use Vue Router's beforeRouteEnter hook to asynchronously load the data via AJAX before the page transitions.

The problem with such an architecture is that it gives us this sub-optimal loading process for the initial page load:

Image title

Eliminating the AJAX request here would make the page interactive much sooner, especially on slow internet connections.

Injecting Initial Application State

If we inject the initial application state into the HTML page, Vue Router won't need to request it from the server, as it will already be available in the client.

We can implement this by JSON-encoding the state server-side and assigning it to a global variable:

index.html

<html>
...
<head>
  ...
  <script type="text/javascript">
   window.__INITIAL_STATE__ = '{ "cars": [ { "id": 1 "name": "Buick", ... }, { ... } ] }'
  </script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

It is then trivial for the app to access and use the state:

let initialState = JSON.parse(window.__INITIAL_STATE__);

new Vue({
  ...
})

This approach eliminates the need for an AJAX request, and reduces the initial app loading process to this:

Image title

I've supplied Lighthouse reports at the bottom of the article to show the improvement in load time.

Note: this approach won't be appropriate if the initial application state includes sensitive data. In that case, you could perhaps do a "hybrid" approach where only non-sensitive data is injected into the page and the sensitive data is retrieved by an authenticated API call.

Implementation in a Multi-Route App

This approach is good enough as-is in an app with only a single route, or if you're happy to inject the initial state of every page within each page requested. But Oldtime Cars has multiple routes, and it'd be much more efficient to only inject the initial state of the current page.

This means we have the following problems to address:

  • How can we determine what initial state to inject into the page request since we don't know what page the user will initially land on?
  • When the user navigates to a different route from within the app, how will the app know whether or not it needs to load new state or just use the injected state?

Vue Router is able to capture any route changes that occur from within the page and handle them without a page refresh. That means clicked links or JavaScript commands that change the browser location.

But route changes from the browser, e.g. the URL bar, or links to the app from external pages, cannot be intercepted by Vue Router and will result in a fresh page load.

Core Concept of the Design Pattern

With that in mind, we need to ensure that each page has the required logic to get its data from either an injection into the page, or via AJAX, depending on whether the page is being loaded freshly from the server, or by Vue Router.

Implementing this is simpler than it sounds, and is best understood through demonstration, so let's go through the code of Oldtime Cars and I'll show you how I did it.

You can see the complete code in this GitHub repo.

Backend Setup

Routes

As the site has two pages, there are two different routes to serve: the home route, and the detail route. The design pattern requires that the routes be served either views, or as JSON payloads, so I've created both web and API routes for each:

routes/web.php

<?php

Route::get('/', 'CarController@home_web');

Route::get('/detail/{id}', 'CarController@detail_web');

routes/api.php

<?php

Route::get('/', 'CarController@home_api');

Route::get('/detail/{id}', 'CarController@detail_api');

Controller

I've abbreviated some of the code to save space, but the main idea is this: the web routes return a view with the initial application state injected into the head of the page (the template is shown below), while the API routes return exactly the same state, only as a payload.

(Also note that in addition to the state, the data includes a path. I'll need this value in the frontend, as you'll see shortly).

app/Http/Controllers/CarController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CarController extends Controller
{

  /* This function returns the data for each car, by id */
  public function get_cars($id) { ... }

  /* Returns a view */
  public function detail_web($id)
  {
      $state = array_merge([ 'path' => '/detail/' . $id], $this->get_cars($id));
      return view('app', ['state' => $state]);
  }

  /* Returns a JSON payload */
  public function detail_api($id)
  {
      $state = array_merge([ 'path' => '/detail/' . $id], $this->get_cars($id));
      return response()->json($state);
  }

  public function home_web() { ... }

  public function home_api() { ... }
}

View

I'm using the same template for each page. Its only notable feature is that it will encode the state as JSON in the head:

resource/views/app.blade.php

<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript">
    window.__INITIAL_STATE__ = "{!! addslashes(json_encode($fields)) !!}";
  </script>
</head>
<body>
  <div id="app"...>
</body>
</html>

Frontend Setup

Router

The frontend of the app uses a standard Vue Router setup. I have a different component for each page, i.e. Home.vue and Detail.vue.

Note that the router is in history mode because I want each route to be treated separately.

resources/assets/js/app.js

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

import Home from './components/Home.vue';
import Detail from './components/Detail.vue';

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/detail/:id', component: Detail }
  ]
});

const app = new Vue({
  el: '#app',
  router
});

Page Components

There's very little going on in the page components. The key logic is in a mixin which I'll show next.

Home.vue

<template>
  <div>
    <h1>Oldtime Cars</h1>
    <div v-for="car in cars"...>
  </div>
</template>
<script>
  import mixin  from '../mixin';

  export default {
    mixins: [ mixin ],
    data() {
      return {
        cars: null
      }
    }
  };
</script>

Mixin

This mixin needs to be added to all the page components, in this case, Home and Detail. Here's how it works:

  1. Adds a beforeRouteEnter hook to each page component. When the app first loads, or whenever the route changes, this hook is called. It, in turn, calls the getData method.
  2. The getData method loads the injected state and inspects the path property. From this, it determines if it can use the injected data, or if it needs to fetch new data. If the latter, it requests the appropriate API endpoint with the Axios HTTP client.
  3. When the promise returned from getData resolves, the beforeRouteEnter hook will use whatever data is returned, and assign it to the data property of that component.

mixin.js

import axios from 'axios';

let getData = function(to) {
  return new Promise((resolve, reject) => {
    let initialState = JSON.parse(window.__INITIAL_STATE__) || {};
    if (!initialState.path || to.path !== initialState.path) {
      axios.get(`/api${to.path}`).then(({ data }) => {
        resolve(data);
      })
    } else {
      resolve(initialState);
    }
  });
};

export default {
  beforeRouteEnter (to, from, next) {
    getData(to).then((data) => {
      next(vm => Object.assign(vm.$data, data))
    });
  }
};

By implementing this mixin, the page components have the required logic to get their initial state from either the data injected into the page, or via AJAX, depending on whether the page loaded from the server, or was navigated to/from Vue Router.

Performance Improvements for Oldtime Cars

I've generated some reports on the app performance using the Lighthouse Chrome extension.

If I skip all of the above and go back to the standard pattern of loading the initial application state from the API, the Lighthouse report is as follows:

Image title

One metric of relevance is the time to first meaningful paint, which here is 2570 ms.

Let's compare this to the improved architecture:

Image title

By loading initial application state from within the page rather than from the API, the time to first meaningful paint went down to 2050 ms, a 25% improvement.

Get the latest Vue.js articles, tutorials, and cool projects in your inbox with the Vue.js Developers Newsletter

Node.js application metrics sent directly to any statsd-compliant system. Get N|Solid

Topics:
vue.js ,javascript ,laravel ,front end development ,web dev

Published at DZone with permission of Anthony Gore, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}