Welcome to Abdul Malik Ikhsan's Blog

Using React.js in Mezzio Application

Posted in Mezzio, React.js, Tutorial PHP by samsonasik on June 25, 2020

Ok, in 3 previous JavaScript posts, I posted how to use Vue.js in Mezzio Application. Now, in this post, I will show you how to use React.js in Mezzio Application.

Let’s start with download the mezzio skeleton:

composer create-project mezzio/mezzio-skeleton mezzio-react

I assume next you choose the following options:

  • Type of Installation: Modular (3)
  • Container: Laminas ServiceManager (3)
  • Router: Laminas Router (3)
  • Template Engine: Laminas View (3)

Now, we are on the same page!

The scenario is same, we want to create an SPA application. In Mezzio part, to make it work, it require template handling for ajax request.

We can create middleware for that:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class XMLHttpRequestTemplateMiddleware implements MiddlewareInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        if (in_array('XMLHttpRequest', $request->getHeader('X-Requested-With'), true)) {
            (function ($template) {
                $template->layout = false;
            })->bindTo($this->template, $this->template)($this->template);
        }

        return $handler->handle($request);
    }
}

In above middleware, we set template layout to false to disable layout when request has X-Requested-With = XmlHttpRequest as an ajax detection. Let’s register above middleware in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\XMLHttpRequestTemplateMiddleware::class => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

and in the pipeline before DispatchMiddleware:

<?php
 // config/pipeline.php
use App\Middleware\XMLHttpRequestTemplateMiddleware;
return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    // ...
};

We need to handle 404 Pages that can work in Ajax request, so we can create a new middleware for that, for example: App\Middleware\NotFoundMiddleware:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class NotFoundMiddleware implements MiddlewareInterface
{
    private $template;
    private $config;

    public function __construct(TemplateRendererInterface $template, array $config)
    {
        $this->template = $template;
        $this->config   = $config;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        return new HtmlResponse(
            $this->template->render($this->config['mezzio']['error_handler']['template_404'])
        );
    }
}

The above middleware need to be registered to ConfigProvider:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\NotFoundMiddleware::class  => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

And then, add to config/pipeline after DispatchMiddleware:

<?php
 // config/pipeline.php

use App\Middleware\NotFoundMiddleware;
use App\Middleware\XMLHttpRequestTemplateMiddleware;

return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    $app->pipe(NotFoundMiddleware::class);
    // ...
};

Now, let’s add About and Contact page handlers:

1. About Page

<?php
// src/App/src/Handler/AboutPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AboutPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::about-page'));
    }
}

With templates:

<!-- src/templates/app/about-page.phtml -->
<h1>About Me</h1>
<p>
    I'm a web developer.
</p>

2. Contact Page

<?php
// src/App/src/Handler/ContactPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ContactPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::contact-page'));
    }
}

With templates:

<!-- src/templates/app/contact-page.phtml -->
<h1>Contact Me</h1>
<p>
    You can contact me via <a href="mailto: foo@bar.baz.com">foo@bar.baz.com</a>
</p>

Handlers Registration

Both AbooutPageHandler and ContactPageHandler need to be registered in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Handler\AboutPageHandler::class => ReflectionBasedAbstractFactory::class,
                Handler\ContactPageHandler::class => ReflectionBasedAbstractFactory::class,
                // ...
            ],
        ];
    }
    // ...
}

and in the routes:

<?php
// config/routes.php
return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->get('/about', App\Handler\AboutPageHandler::class, 'about');
    $app->get('/contact', App\Handler\ContactPageHandler::class, 'contact');
};

Now, we have 3 html pages: Home, About, and Contact. It’s enough for demonstration.

JS dependencies

We can register js dependencies in the layout by add the following js:

<?php
// src/App/templates/layout/default.phtml

$this->headLink()
    ->prependStylesheet('https://use.fontawesome.com/releases/v5.12.1/css/all.css')
    ->prependStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css');
$this->inlineScript()

    ->prependFile('/js/app.js', 'module')
    ->prependFile('https://unpkg.com/react-router-dom@5.2.0/umd/react-router-dom.js')
    ->prependFile('https://unpkg.com/html-react-parser@0.13.0/dist/html-react-parser.js')
    ->prependFile('https://unpkg.com/dompurify@2.0.12/dist/purify.js')
    ->prependFile('https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js')
    ->prependFile('https://unpkg.com/react@16.13.1/umd/react.production.min.js')

    ->prependFile('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js')
    ->prependFile('https://code.jquery.com/jquery-3.5.0.min.js');
?>

React dependencies are react, react-dom, html-react-parser, and react-router-dom. The dompurify will be used to purify the js before displaying. For /js/app.js, that’s our code in public directory to make routing definition. In the layout, we need element for mounting, let’s define is as “root” under body:

<!-- // src/App/templates/layout/default.phtml -->
<body class="app">
    <div id="root">
        
    </div>
    <?=$this->inlineScript()?>
</body>

Now, we can define a Page component creator function, eg: createPage(), we can create a js file for it that will be consumed by /js/app.js under public directory later:

// public/js/create-page.js
let createPage = (title) => class Page extends React.Component {
    constructor(props) {
        super(props);
        this.state = { content: ''};
    }

    componentDidMount() {
        new Promise( (resolve) => {
            fetch(
                this.props.location.pathname,
                {
                    method: 'GET',
                    headers: {
                        'X-Requested-With': 'XMLHttpRequest',
                    }
                }
            ).then(response =>  resolve(response.text()));
        }).then(result => {
            this.setState({ content : result });
            document.title = title;
        });
    }

    render() {
      return React.createElement(
            'div',
            {
                className : "app-content",
            },
            React.createElement(
                "main",
                {
                    className : "container"
                },
                HTMLReactParser(DOMPurify.sanitize(this.state.content))
            )
        );
    }
}

export default createPage;

Above, we use location route path page, and change title after content grabbed based on passed title parameter. We use HTMLReactParser to generate html for the raw html from the server response.

Now, let create a Navigation, we can create Navigation component, eg, in /js/Navigation.js under public directory:

// public/js/Navigation.js
const {
    NavLink
} = ReactRouterDOM;

const {
    Component,
    createElement
} = React;

class Navigation extends Component {
    render() {
      return createElement(
            'nav',
            {
                className : "navbar navbar-expand-sm navbar-dark bg-dark fixed-top",
                role: "navigation"
            },
            createElement(
                'div',
                {
                    className: "container"
                },
                createElement(
                    'div',
                    {
                        className: "navbar-header"
                    },
                    createElement(
                        'button',
                        {
                            className: "navbar-toggler",
                            "data-toggle": "collapse",
                            "data-target": "#navbarCollapse",
                            "aria-controls": "#navbarCollapse",
                            "aria-expanded": "false",
                            "aria-label": "Toggle navigation"
                        },
                        createElement(
                            'span',
                            {
                                className: "navbar-toggler-icon"
                            }
                        )
                    ),
                    createElement(
                        NavLink,
                        {
                            to: "/",
                            className: "navbar-brand"
                        },
                        createElement(
                            'img',
                            {
                                src: "https://docs.laminas.dev/img/laminas-mezzio-rgb.svg",
                                alt: "Laminas Mezzio",
                                height: 56
                            }
                        )
                    )
                ),
                createElement(
                    'div',
                    {
                        className: "collapse navbar-collapse",
                        id: "navbarCollapse"
                    },
                    createElement(
                        "ul",
                        {
                            className: "navbar-nav mr-auto"
                        },
                        createElement(
                            "li",
                            {
                                className: "nav-item"
                            },
                            createElement(NavLink, { className: 'nav-link', to: "/", exact: true }, "Home")
                        ),
                        createElement(
                            "li",
                            {
                                className: "nav-item"
                            },
                            createElement(NavLink, { className: 'nav-link', to: "/about", exact: true }, "About")
                        ),
                        createElement(
                            "li",
                            {
                                className: "nav-item"
                            },
                            createElement(NavLink, { className: 'nav-link', to: "/contact", exact: true }, "Contact")
                        )
                    )
                )
            )
        );
    }
}

export default Navigation;

Above, we define the navigation, with add “active” class on link selected.

Finally, our /js/app.js under public directory that consume createPage function and Navigation component to be used for routing definition and navigation.

// public/js/app.js
import createPage from './create-page.js';
import Navigation from './Navigation.js';

const {
    BrowserRouter,
    Switch,
    Route
} = ReactRouterDOM;

const Main = () => React.createElement(
    "main",
    null,
    React.createElement(
        Switch,
        null,
        React.createElement(
            Route, {
                exact: true,
                path: "/",
                component: createPage('Home')
            }
        ),
        React.createElement(
            Route, {
                exact: true,
                path: "/about",
                component: createPage('About')
            }
        ),
        React.createElement(
            Route, {
                exact: true,
                path: "/contact",
                component: createPage('Contact')
            }
        ),
        React.createElement(
            Route, {
                exact: true,
                path: "*",
                component: createPage('404 Page')
            }
        )
    )
);

const Header = () => React.createElement(
    'header',
    {
        className: 'app-header'
    },
    React.createElement(Navigation)
);

const App = () => React.createElement(
    "div",
    null,
    React.createElement(Header, null),
    React.createElement(Main, null)
);

ReactDOM.render(
    React.createElement(
        BrowserRouter,
        null,
        React.createElement(App, null)
    ),
    document.getElementById('root')
);

// https://reactjs.org/docs/react-without-jsx.html
// https://www.pluralsight.com/guides/just-plain-react
// https://codepen.io/pshrmn/pen/YZXZqM?editors=1010

Now, if we check, we will get SPA working:

That’s it! I uploaded the sample source code at github: https://github.com/samsonasik/mezzio-react

References:
https://reactjs.org/docs/react-without-jsx.html
https://www.pluralsight.com/guides/just-plain-react
https://codepen.io/pshrmn/pen/YZXZqM?editors=1010

Tagged with: ,

Using Vue.js in Mezzio Application

Posted in Javascript, Mezzio, Tutorial PHP, Vue.js by samsonasik on May 30, 2020

So, another JavaScript post! If you read my post at 2015 about Ember.js usage in Zend Framework 2 application, now let’s try with Vue.js, but for Mezzio application.

Let’s start with download the mezzio skeleton:

composer create-project mezzio/mezzio-skeleton mezzio-vue

I assume next you choose the following options:

  • Type of Installation: Modular (3)
  • Container: Laminas ServiceManager (3)
  • Router: Laminas Router (3)
  • Template Engine: Laminas View (3)

Now, we are on the same page!

The scenario is same, we want to create an SPA application. In Mezzio part, to make it work, it require template handling for ajax request.

We can create middleware for that:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class XMLHttpRequestTemplateMiddleware implements MiddlewareInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        if (in_array('XMLHttpRequest', $request->getHeader('X-Requested-With'), true)) {
            (function ($template) {
                $template->layout = false;
            })->bindTo($this->template, $this->template)($this->template);
        }

        return $handler->handle($request);
    }
}

In above middleware, we set template layout to false to disable layout when request has X-Requested-With = XmlHttpRequest as an ajax detection. Let’s register above middleware in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\XMLHttpRequestTemplateMiddleware::class => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

and in the pipeline before DispatchMiddleware:

<?php
 // config/pipeline.php
use App\Middleware\XMLHttpRequestTemplateMiddleware;
return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    // ...
};

We need to handle 404 Pages that can work in Ajax request, so we can create a new middleware for that, for example: App\Middleware\NotFoundMiddleware:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class NotFoundMiddleware implements MiddlewareInterface
{
    private $template;
    private $config;

    public function __construct(TemplateRendererInterface $template, array $config)
    {
        $this->template = $template;
        $this->config   = $config;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        return new HtmlResponse(
            $this->template->render($this->config['mezzio']['error_handler']['template_404'])
        );
    }
}

The above middleware need to be registered to ConfigProvider:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\NotFoundMiddleware::class  => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

And then, add to config/pipeline after DispatchMiddleware:

<?php
 // config/pipeline.php

use App\Middleware\NotFoundMiddleware;
use App\Middleware\XMLHttpRequestTemplateMiddleware;

return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    $app->pipe(NotFoundMiddleware::class);
    // ...
};

Now, let’s add About and Contact page handlers:

1. About Page

<?php
// src/App/src/Handler/AboutPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AboutPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::about-page'));
    }
}

With templates:

<!-- src/templates/app/about-page.phtml -->
<h1>About Me</h1>
<p>
    I'm a web developer.
</p>

2. Contact Page

<?php
// src/App/src/Handler/ContactPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ContactPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::contact-page'));
    }
}

With templates:

<!-- src/templates/app/contact-page.phtml -->
<h1>Contact Me</h1>
<p>
    You can contact me via <a href="mailto: foo@bar.baz.com">foo@bar.baz.com</a>
</p>

Handlers Registration

Both AbooutPageHandler and ContactPageHandler need to be registered in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Handler\AboutPageHandler::class => ReflectionBasedAbstractFactory::class,
                Handler\ContactPageHandler::class => ReflectionBasedAbstractFactory::class,
                // ...
            ],
        ];
    }
    // ...
}

and in the routes:

<?php
// config/routes.php
return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->get('/about', App\Handler\AboutPageHandler::class, 'about');
    $app->get('/contact', App\Handler\ContactPageHandler::class, 'contact');
};

Now, we have 3 html pages: Home, About, and Contact. It’s enough for demonstration.

JS dependencies

We can register js dependencies in the layout by add the following js:

<?php
// src/App/templates/layout/default.phtml

$this->headLink()
    ->prependStylesheet('https://use.fontawesome.com/releases/v5.12.1/css/all.css')
    ->prependStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css');
$this->inlineScript()

    ->prependFile('/js/app.js')
    ->prependFile('https://unpkg.com/vue-router@3.3.2/dist/vue-router.js')
    ->prependFile('https://unpkg.com/vue@2.6.11/dist/vue.js')

    ->prependFile('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js')
    ->prependFile('https://code.jquery.com/jquery-3.5.0.min.js');
?>

Vue dependencies are Vue.js core and Vue Router for routing. For /js/app.js, that’s our code to make routing definition. In the layout, we need element for mounting, let’s define is as “root” under body:

<!-- // src/App/templates/layout/default.phtml -->
<body class="app">
    <div id="root">
        
    </div>
    <?=$this->inlineScript()?>
</body>

Now, we can define the router links inside “root” div:

<!-- // src/App/templates/layout/default.phtml -->
<div id="root">

        <header class="app-header">
            <nav class="navbar navbar-expand-sm navbar-dark bg-dark fixed-top" role="navigation">
                <div class="container">
                    <div class="navbar-header">
                        <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="#navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                            <span class="navbar-toggler-icon"></span>
                        </button>
                        <router-link to="/" class="navbar-brand"><img src="https://docs.laminas.dev/img/laminas-mezzio-rgb.svg" alt="Laminas Mezzio" height="56" /></router-link>
                    </div>
                    <div class="collapse navbar-collapse" id="navbarCollapse">
                        <ul class="navbar-nav mr-auto">
                            <li class="nav-item">
                                <router-link to="/" class="nav-link">Home</router-link>
                            </li>
                            <li class="nav-item">
                                <router-link to="/about" class="nav-link">About</router-link>
                            </li>
                            <li class="nav-item">
                                <router-link to="/contact" class="nav-link">Contact</router-link>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>
        </header>

</div>

Next, time for the view content placeholder, we can define router-view inside “root” div as well for place to collect the content:

<!-- // src/App/templates/layout/default.phtml -->
<div id="root">
        <header class="app-header"> <!-- content app header before --> </header>

        <main class="container">
            <div id="app">
                <keep-alive> <!-- make content cached if already loaded -->
                    <router-view></router-view>
                </keep-alive>
            </div>
        </main>

        <!-- footer here -->
</div>

Now, our JS file in public/js/app.js for routing definition:

createPage = (name) => {
    return Vue.component('page-' + name, {
        data: () => {
            return  {
              content: 'Loading...'
            }
        },
        mounted () {
            (new Promise( (resolve) => {
                fetch(
                    this.$route.path,
                    {
                        method: 'GET',
                        headers: {
                            'X-Requested-With': 'XMLHttpRequest',
                        }
                    }
                ).then(response =>  resolve(response.text()));
            })).then(result => this.content = result);
        },
        template: '<div v-html="content"></div>'
    });
}

const routes = [
    { path: '/', component: createPage('home') },
    { path: '/about', component: createPage('about') },
    { path: '/contact', component: createPage('contact') },
    { path: '*', component: createPage('404') }
];

const router = new VueRouter({
    routes,
    base: '/',
    mode: 'history',
    linkExactActiveClass: "active"
});
const app    = new Vue({router}).$mount('#root')

In above code, we create a page component on the fly via function createPage and apply to each path. Setup VueRouter with routes definition, and make a Vue instance with it, mount to div id #root.

Bonus

How about make page title changed after on change page? We can set meta title in each route definition, and use router.afterEach() to apply it:

// ...
const routes = [
	{ path: '/', component: createPage('home'),  meta: {
            title: 'Home'
        } },
	{ path: '/about', component: createPage('about'), meta: {
            title: 'About Me'
        } },
	{ path: '/contact', component: createPage('contact'), meta: {
            title: 'Contact Me'
        } },
    { path: '*', component: createPage('404'), meta: {
            title: '404 Not Found'
        } }
];

const router = new VueRouter({
    routes,
    base: '/',
    mode: 'history',
    linkExactActiveClass: "active"
});

router.afterEach(to => document.title = to.meta.title);
// ...

Now, if we check, we will get SPA working:

That’s it! I uploaded the sample source code at github: https://github.com/samsonasik/mezzio-vue

References:
https://vuejs.org/v2/guide/
https://router.vuejs.org/guide/#html
https://medium.com/badr-interactive/mengenal-lifecycle-hooks-pada-vue-js-78cd2225a69
https://forum.vuejs.org/t/setting-a-correct-base-url-with-vue-router/24726/2
https://forum.vuejs.org/t/how-do-i-make-an-html-tag-inside-a-data-string-render-as-an-html-tag/13074/3