Create a Single Page Application with Laravel and Vue

With its clean syntax and expressiveness, among many other qualities, Laravel is one of the most popular PHP frameworks used by developers.

Before Laravel UI came out, one of its main features was to support Vue.js by default from Laravel v5.3 to v6. Vue is a modern JavaScript front-end framework for creating user interfaces.

Why Laravel and Vue go so well together?

Here are some key advantages of using Laravel with Vue to create a full-stack workflow for your projects:

  • Source code is merged into one project instead of having separate projects for backend and frontend

  • Simple setup and configuration

  • A single deployment can handle both frameworks

What is a spa? (single page application)

How does a single-page application (SPA for short) enter the BIOS settings in Windows 10? Detailed eight-step graphic tutorial New data is dynamically loaded from the web server to the web page without refreshing the entire page.

Examples of popular sites that use SPAs include gmail.com and youtube.com - in other words, SPAs are largely ubiquitous. Most of the admin dashboards you probably use on a daily basis were created using SPA.

Benefits of Spa:

  • User experience is more flexible

  • cache data in the browser

  • fast loading time

Disadvantages of SPA:

  • May harm SEO (Search Engine Optimization)

  • potential security issues

  • Consumes a lot of browser resources

project settings

This post will demonstrate how to develop a to-do application that allows users to register for an account and add tasks.

In this tutorial, Laravel 9 is used, which requires PHP 8.1 and Vue 3; we also need PHP and NGINX installed.

Let's start with the following command:

composer create-project --prefer-dist laravel/laravel laravel-vue-demo

Next, we'll install the JavaScript dependencies.

npm install

Before we can add Vue to our project, we must install some packages.

In addition, plugin-vue must be installed. How can Xiaomi mobile phones test whether the hardware is broken? How to detect Xiaomi mobile phone by itself? Because Laravel 9 ships with Vite instead of webpack-mix which is the Laravel bundler for JavaScript. Now let's do this:

npm install vue@next vue-loader@next @vitejs/plugin-vue

Open the called file and add to the configuration: vite.config.js``vue()

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'
​
export default defineConfig({
    plugins: [
        vue(),
        laravel([
            'resources/css/app.css',
            'resources/js/app.js',
        ]),
    ],
});

Edit the file and code snippet for the Vue 3 app bootstrap: app.js

require('./bootstrap');
​
import {createApp} from 'vue'
​
import App from './App.vue'
​
createApp(App).mount("#app")

Create a file called and add the following content: App.vue

<template>
  <h1> Hello, Vuejs with Laravel </h1>
</template>
<script>
export default {
  setup() {
​
   }
}
</script>

Finally, open the file located in the folder and add the following content: welcome.blade.php ``resources/views

<!DOCTYPE html>
<html>
<head>
 ....
        @vite('resources/css/app.css')
</head>
<body>
  <div id="app"></div>
  @vite('resources/js/app.js')
</body>
</html>

To preview our application, we need to start our Vue application and Laravel server on two different terminals/command lines :

npm run dev
​
​
php artisan serve

To build our to-do application, we need to create some more files. Vue will create several pages, mainly:

  • log in page

  • registration page

  • home page

To communicate with Laravel endpoints, we need to install Axios:

npm install axios

Vue routing

Various routing strategies are available in Vue using the vue-router package ; these strategies are also known as history patterns.

When a user requests a route like this (which will return a 404 error when the page is refreshed), we can rely on Laravel to detect any fallback routes and then serve up the Blade file containing our application. http://localhost:8000/home

Therefore, we will use HTML5 mode:

Route::get('/{vue_capture?}', function() {
    return view('welcome');
})->where('vue_capture', '[\/\w\.-]*');
import {createRouter, createWebHistory} from 'vue-router';

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/',
            component: () => import('./pages/Login.vue')
        },
        {
            path: '/register',
            component: () => import('./pages/Register.vue')
        },
        {
            path: '/home',
            component: () => import('./pages/Home.vue')
        }
    ],
})

Due to the simplicity of the project, we're essentially using Laravel Sanctum to handle authentication for the login page, and then saving our tokens in local storage.

In order for other requests to succeed, the token will be attached to the header, which will allow the requesting user to be identified by Laravel.

Here is our login page:

Below is our registration page:

Finally, here are the relevant code blocks for both:

<!--Login.vue-->
<template>
    <div class="mx-auto w-4/12 mt-10 bg-blue-200 p-4 rounded-lg">
        <div
            class="bg-white shadow-lg rounded-lg px-8 pt-6 pb-8 mb-2 flex flex-col"
        >
            <h1 class="text-gray-600 py-5 font-bold text-3xl"> Login </h1>
            <ul class="list-disc text-red-400" v-for="(value, index) in errors" :key="index" v-if="typeof errors === 'object'">
                <li>{
  
  {value[0]}}</li>
            </ul>
            <p class="list-disc text-red-400" v-if="typeof errors === 'string'">{
  
  {errors}}</p>
            <form method="post" @submit.prevent="handleLogin">
            <div class="mb-4">
                <label
                    class="block text-grey-darker text-sm font-bold mb-2"
                    for="username"
                >
                    Email Address
                </label>
                <input
                    class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker"
                    id="username"
                    type="text"
                    v-model="form.email"
                    required
                />
            </div>
            <div class="mb-4">
                <label
                    class="block text-grey-darker text-sm font-bold mb-2"
                    for="password"
                >
                    Password
                </label>
                <input
                    class="shadow appearance-none border border-red rounded w-full py-2 px-3 text-grey-darker mb-3"
                    id="password"
                    type="password"
                    v-model="form.password"
                    required
                />
            </div>
            <div class="flex items-center justify-between">
                <button
                    class="bg-blue-500 hover:bg-blue-900 text-white font-bold py-2 px-4 rounded"
                    type="submit"
                >
                    Sign In
                </button>
                <router-link
                    class="inline-block align-baseline font-bold text-sm text-blue hover:text-blue-darker"
                    to="register"
                >
                    Sign Up
                </router-link>
            </div>
            </form>
        </div>
    </div>
</template>
export default {
    setup() {
        const errors = ref()
        const router = useRouter();
        const form = reactive({
            email: '',
            password: '',
        })
        const handleLogin = async () => {
            try {
                const result = await axios.post('/api/auth/login', form)
                if (result.status === 200 && result.data && result.data.token) {
                    localStorage.setItem('APP_DEMO_USER_TOKEN', result.data.token)
                    await router.push('home')
                }
            } catch (e) {
                if(e && e.response.data && e.response.data.errors) {
                    errors.value = Object.values(e.response.data.errors)
                } else {
                    errors.value = e.response.data.message || ""
                }
            }
        }

        return {
            form,
            errors,
            handleLogin,
        }
    }
}

Vue views/pages handle all todos like creating, deleting, updating and listing todos. All operations make requests to the endpoint with a user token for authorization through Axios. Home

Let's see what they look like, followed by the relevant snippets:

<!-- Home -->
<template>
    <div class="w-6/12 p-10 mx-auto">
        <div class="flex justify-between">
            <h1 class="text-2xl"> Todo </h1>
            <span class="capitalize">Welcome {
  
  { user && user.name }}, <button
                class="text-orange-500 underline hover:no-underline rounded-md"
                @click="handleLogout">Logout</button></span>
        </div>
        <input type="text" class="p-2 w-64 border rounded-md" v-model="todo" placeholder="Enter your todo"/>
        <button class="bg-blue-600 text-white px-5 py-2 rounded-md ml-2 hover:bg-blue-400" @click="addTodo">Add</button>
        <Loader v-if="isLoading"/>
        <ul class="border-t mt-3 cursor-pointer">
            <li :class="`py-3 border-b text-gray-600 ${val.has_completed ? 'line-through' : ''}`"
                v-for="(val, idx) in todos" :key="idx">
                <input type="checkbox" :checked="val.has_completed" @click="checked(idx)"/>
                <span @click="checked(val, idx)" class="pl-3">{
  
  { val.title }} </span>
                <button class="float-right bg-red-400 px-2 text-white font-bold rounded-md hover:bg-red-600"
                        @click="deleteTodo(val, idx)">&times;
                </button>
            </li>
        </ul>
    </div>
</template>
setup() {
    const todo = ref('')
    const todos = ref([])
    const user = ref()
    const isLoading = ref()

    let router = useRouter();
    onMounted(() => {
        authentication()
        handleTodos()
    });

    const authentication = async () => {
        isLoading.value = true
        try {
            const req = await request('get', '/api/user')
            user.value = req.data
        } catch (e) {
            await router.push('/')
        }
    }

    const handleTodos = async () => {
        try {
            const req = await request('get','/api/all')
            todos.value = req.data.data
        } catch (e) {
            await router.push('/')
        }
        isLoading.value = false
    }

    const handleNewTodo = async (title) => {
        try {
            const data = {title: title}
            const req = await request('post', '/api/todos', data)
            if (req.data.message) {
                isLoading.value = false
                return alert(req.data.message)
            }
            todos.value.push(req.data.data)
        } catch (e) {
            await router.push('/')
        }
        isLoading.value = false
    }

    const handleLogout = () => {
        localStorage.removeItem('APP_DEMO_USER_TOKEN')
        router.push('/')
    }

    const addTodo = () => {
        if (todo.value === "") {
            return alert("Todo cannot be empty");
        }
        isLoading.value = true
        handleNewTodo(todo.value)
        todo.value = ""
    }

    const checked = async (val, index) => {
        try {
            const data = {has_completed: !val.has_completed}
            const req = await request('put', `/api/todos/${val.id}`, data)
            if (req.data.message) {
                isLoading.value = false
                return alert(req.data.message)
            }
            todos.value[index].has_completed = !val.has_completed
        } catch (e) {
            await router.push('/')
        }
        isLoading.value = false
    }

    const deleteTodo = async (val, index) => {
        if (window.confirm("Are you sure")) {
            try {
                const req = await request('delete', `/api/todos/${val.id}`)
                if (req.data.message) {
                    isLoading.value = false
                    todos.value.splice(index, 1)
                }
            } catch (e) {
                await router.push('/')
            }
            isLoading.value = false
        }
    }

For Laravel we will create the following:

  • controller(AuthController``TodoController)

  • model(,Todo``User)

  • route (api)

  • middleware(). auth: sanctum

Our route is in , which handles all the endpoints Vue uses. api.php

Route::post('/auth/register', [AuthController::class, 'register']);
Route::post('/auth/login', [AuthController::class, 'login']);

Route::apiResource('todos', TodoController::class)->middleware('auth:sanctum');

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

The register logic in registers the user and creates the token. AuthController

public function register(Request $request): \Illuminate\Http\JsonResponse
{
    try {
        //Validated
        $validateUser = Validator::make($request->all(),
        [
            'name' => 'required',
            'email' => 'required|email|unique:users,email',
            'password' => 'required'
        ]);

        if($validateUser->fails()){
            return response()->json([
                'status' => false,
                'message' => 'validation error',
                'errors' => $validateUser->errors()
            ], 401);
        }

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password)
        ], 201);

        return response()->json([
            'status' => true,
            'message' => 'User Created Successfully',
            'token' => $user->createToken("API_TOKEN")->plainTextToken
        ], 200);

    } catch (\Throwable $e) {
        return response()->json([
            'status' => false,
            'message' => $e->getMessage()
        ], 500);
    }
}

A login verification request, checks that the user exists, then creates a token:

public function login(Request $request): \Illuminate\Http\JsonResponse
{
    try {
        //Validated
        $validateUser = Validator::make($request->all(),
            [
                'email' => 'required',
                'password' => 'required'
            ]);
​
        if($validateUser->fails()){
            return response()->json([
                'status' => false,
                'message' => 'validation error',
                'errors' => $validateUser->errors()
            ], 401);
        }
​
        if(!Auth::attempt($request->only(['email', 'password']))){
            return response()->json([
                'status' => false,
                'message' => 'Email & Password does not exist.',
            ], 401);
        }
​
        $user = User::where('email', $request->email)->first();
​
        return response()->json([
            'status' => true,
            'message' => 'Logged In Successfully',
            'token' => $user->createToken("API_TOKEN")->plainTextToken
        ], 200);
​
    } catch (\Throwable $e) {
        return response()->json([
            'status' => false,
            'message' => $e->getMessage()
        ], 500);
    }
}

The POST endpoint for adding a new todo is managed by a method in the todo controller — . store``/api/todos

public function store(Request $request): \Illuminate\Http\JsonResponse
{
    $data = Todo::where('user_id', $request->user()->id)->where('title', $request->title);
    if ($data->first()) {
        return response()->json(['status' => false, 'message' => 'Already exist']);
    }
    $req = $request->all();
    $req['user_id'] = $request->user()->id;
    $data = Todo::create($req);
    return response()->json(['status' => true, 'data' => $data], 201);
}

This will manage the update endpoint, which is called when the user completes a todo, at . Todo``/api/todos/id

public function update(Request $request, $id): \Illuminate\Http\JsonResponse
{
    $validateUser = Validator::make($request->all(),
        [
            'has_completed' => 'required',
        ]);

    if ($validateUser->fails()) {
        return response()->json([
            'status' => false,
            'message' => 'validation error',
            'errors' => $validateUser->errors()
        ], 401);
    }

    $data = Todo::find($id);
    $data->has_completed = $request->has_completed;
    $data->update();
    return response()->json(['status' => true, 'data' => $data], 202);
}

When a user deletes, the endpoint will be called, which will handle it: Todo``/api/todos/id

public function destroy(int $id): \Illuminate\Http\JsonResponse
{
    throw_if(!$id, 'todo Id is missing');
    Todo::findOrFail($id)->delete();
    return response()->json(['status' => true, 'message' => 'todo deleted']);
}

Great job, we created a single page application using Laravel and Vue! Below is the final result of our todo application and Well done, we created a single page application using Laravel and Vue! :

in conclusion

We were able to determine that it was much simpler to create a simple user authentication and to-do list application with Vue and Laravel than with a traditional PHP/Vue combo - which probably required more setup work.

In my opinion, the combination of Vue and Laravel makes it easy to develop single page applications because there is no need to worry about routing, middleware or dealing with CORS. Let me know about your experiences developing SPAs in the comments section below.

Guess you like

Origin blog.csdn.net/weixin_47967031/article/details/132590611