The Laravel Developer's The Laravel Developer's Guide to Vue SPAs - - PowerPoint PPT Presentation

the laravel developer s the laravel developer s guide to
SMART_READER_LITE
LIVE PREVIEW

The Laravel Developer's The Laravel Developer's Guide to Vue SPAs - - PowerPoint PPT Presentation

The Laravel Developer's The Laravel Developer's Guide to Vue SPAs Guide to Vue SPAs JESS ARCHER The BaseCode Podcast The BaseCode Podcast basecodefieldguide.com/podcast Social wish list and gift reminders Social wish list and gift reminders


slide-1
SLIDE 1

The Laravel Developer's The Laravel Developer's Guide to Vue SPAs Guide to Vue SPAs

JESS ARCHER

slide-2
SLIDE 2
slide-3
SLIDE 3
slide-4
SLIDE 4

The BaseCode Podcast The BaseCode Podcast

basecodefieldguide.com/podcast

slide-5
SLIDE 5
slide-6
SLIDE 6

Social wish list and gift reminders Social wish list and gift reminders

slide-7
SLIDE 7

What is an SPA? What is an SPA?

(Single Page Application) (Single Page Application)

slide-8
SLIDE 8

What is an SPA? What is an SPA?

(Single Full Page Load Single Full Page Load Application) Application)

slide-9
SLIDE 9

Why build an SPA? Why build an SPA?

slide-10
SLIDE 10

“It feels like an app” “It feels like an app”

— Everyone

slide-11
SLIDE 11

Code Sharing Code Sharing

slide-12
SLIDE 12
slide-13
SLIDE 13

<!-- MyComponent.vue --> <template web> <input v-model="name" /> </template> <template native> <TextField v-model="name" /> </template> <script> export default { // shared logic } </script>

nativescriptvue.org

slide-14
SLIDE 14

Why not build an SPA? Why not build an SPA?

slide-15
SLIDE 15

We need to re-engineer things We need to re-engineer things that the browser ordinarily give us that the browser ordinarily give us

slide-16
SLIDE 16

Easy to harm user experience Easy to harm user experience and accessibility and accessibility

slide-17
SLIDE 17

Hard to optimise for search engines Hard to optimise for search engines

slide-18
SLIDE 18

More work More work Slower to build Slower to build More to maintain More to maintain

slide-19
SLIDE 19

SPA SPA Authentication Authentication

slide-20
SLIDE 20

Goals Goals

(assumptions) (assumptions)

  • 1. The SPA should be completely decoupled

The SPA should be completely decoupled from the back-end from the back-end

  • 2. OAuth2 and JWTs are the gold standards

OAuth2 and JWTs are the gold standards for authentication for authentication

slide-21
SLIDE 21

OAuth OAuth

Primarily intended for third-party auth

slide-22
SLIDE 22

OAuth OAuth

Requires the client to hold tokens or secrets

slide-23
SLIDE 23
slide-24
SLIDE 24
slide-25
SLIDE 25

Short access tokens... Refresh regularly...

😭

(Sorry Alex)

slide-26
SLIDE 26

Why short access tokens Why short access tokens and refresh regularly? and refresh regularly?

slide-27
SLIDE 27

If JavaScript can access a token If JavaScript can access a token it is vulnerable to XSS attacks. it is vulnerable to XSS attacks.

slide-28
SLIDE 28

JWT JWT

JSON Web Tokens

slide-29
SLIDE 29

JWT JWT

Really clever way to do decentralised and stateless authentication

slide-30
SLIDE 30

JWT JWT

JavaScript clients need to hold the token

slide-31
SLIDE 31

I felt like I kept I felt like I kept hitting walls... hitting walls...

slide-32
SLIDE 32
slide-33
SLIDE 33

The solution? The solution?

slide-34
SLIDE 34

🍫

Cookies! Cookies!

slide-35
SLIDE 35

Cookies marked Cookies marked HttpOnly HttpOnly cannot be cannot be accessed by JavaScript accessed by JavaScript

slide-36
SLIDE 36 🙆 Can't use Laravel's default token-based API middleware 🤣 Could put your API routes in routes/web.php to use

the session cookie

🤯 Could change your routes/api.php middleware to

use sessions Or...

slide-37
SLIDE 37

Laravel Passport Laravel Passport

It's not just for OAuth! It's not just for OAuth!

(But it also does OAuth!) (But it also does OAuth!)

slide-38
SLIDE 38

Passport can authenticate Passport can authenticate via cookies too! via cookies too!

slide-39
SLIDE 39

// app/Http/Kernel.php 'web' => [ // Other middleware... \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class, ],

slide-40
SLIDE 40

// resources/js/bootstrap.js window.axios.interceptors.response.use(undefined, function (error) { switch (error.response.status) { case 401: // Not logged in case 419: // Session expired case 503: // Down for maintenance // Bounce the user to the login screen with a redirect back window.location.reload() break; case 500: alert('Ooops, something went wrong! The team have been notified.') break; default: // Allow individual requests to handle other errors return Promise.reject(error); } });

slide-41
SLIDE 41

# .env SESSION_LIFETIME=43200 # 30 days

slide-42
SLIDE 42

State Management State Management

slide-43
SLIDE 43

<h1>Hi, {{ name }}</h1> name: 'Laracon', <template> </template> <script> export default { data() { return { } } } </script>

slide-44
SLIDE 44

<h1>Count: {{ count }}</h1> props: [ 'count' ], <template> <button @click="$emit('increment')"> Increment </button> </template> <script> export default { } </script>

slide-45
SLIDE 45

<button @click="$emit('increment')"> <template> <h1>Count: {{ count }}</h1> Increment </button> </template> <script> export default { props: [ 'count' ], } </script>

slide-46
SLIDE 46

One-Way Data Flow One-Way Data Flow

https:vuejs.org/v2/guide/componentsprops.html

slide-47
SLIDE 47

Store Pattern Store Pattern

slide-48
SLIDE 48

Demo Demo

Don't worry - no live coding on my rst talk 😆

slide-49
SLIDE 49

import Vuex from 'vuex' const store = new Vuex.Store({ state: {}, // data getters: {}, // computed properties mutations: {}, // methods that change state actions: {} // arbitrary async methods })

slide-50
SLIDE 50

Freedom can be paralysing in Freedom can be paralysing in software development software development

slide-51
SLIDE 51

Vuex as the interface to my API Vuex as the interface to my API and single source of truth for my data and single source of truth for my data

slide-52
SLIDE 52

I only push data to Vuex when I only push data to Vuex when it's ready to be saved to the server. it's ready to be saved to the server.

// MyComponent.vue methods: { save() { this.$store.dispatch('luckyDucks/save', this.luckyDuck) } }

slide-53
SLIDE 53

I place my API requests inside I place my API requests inside Vuex "Actions" Vuex "Actions"

// store/modules/lucky-ducks.js actions: { save(context, luckyDuck) { axios.post('/api/lucky-ducks', luckyDuck) // ... } }

slide-54
SLIDE 54

I store the data returned from the server, I store the data returned from the server, not what was passed to the Vuex action. not what was passed to the Vuex action.

// store/modules/lucky-ducks.js axios.post('/api/lucky-ducks', luckyDuck) .then(response => { context.commit('save', response.data.data) })

slide-55
SLIDE 55

modules: { luckyDucks,

  • ccasions,

} import Vuex from 'vuex' import luckyDucks from 'modules/lucky-ducks.js' import occasions from 'modules/occasions.js' ... const store = new Vuex.Store({ ... })

slide-56
SLIDE 56

Structuring your data Structuring your data

  • n the front end
  • n the front end
slide-57
SLIDE 57

class LuckyDuckController { public function index() { return auth()

  • >user()
  • >luckyDucks
  • >load('occasions');

} }

slide-58
SLIDE 58

[ { "id": 1, "nickname": "Sally", "occasions": [ { "id": 1, "type": "birthday", "date": "1989-11-30" }, ] }, ] ... ...

slide-59
SLIDE 59

I thought I'd set up everything I thought I'd set up everything perfectly for my front-end... perfectly for my front-end...

slide-60
SLIDE 60

What I wanted... What I wanted...

// Occasions belonging to an individual lucky duck luckyDuck.occasions.forEach(occasion => {

  • ccasion.doSomething()

}) // All occasions, regardless of lucky duck

  • ccasions.forEach(occasion => {
  • ccasion.doSomething()

}) // Individual occasions by ID

  • ccasion = findById(123)
slide-61
SLIDE 61

Flatten your data Flatten your data

  • n the front end
  • n the front end
slide-62
SLIDE 62

state: { luckyDucks: { 1: { id: 1, nickname: 'Sally',

  • ccasions: [1, 2, 3]

}, },

  • ccasions: {

1: { id: 1, type: 'birthday', date: '1989-11-30' lucky_duck_id: 1 }, } } ... ...

slide-63
SLIDE 63

luckyDucks: { },

  • ccasions: {

} state: { 1: { id: 1, nickname: 'Sally',

  • ccasions: [1, 2, 3]

}, ... 1: { id: 1, type: 'birthday', date: '1989-11-30' lucky_duck_id: 1 }, ... }

slide-64
SLIDE 64

1: { 1: { state: { luckyDucks: { id: 1, nickname: 'Sally',

  • ccasions: [1, 2, 3]

}, ... },

  • ccasions: {

id: 1, type: 'birthday', date: '1989-11-30' lucky_duck_id: 1 }, ... } }

slide-65
SLIDE 65

id: 1, id: 1, state: { luckyDucks: { 1: { nickname: 'Sally',

  • ccasions: [1, 2, 3]

}, ... },

  • ccasions: {

1: { type: 'birthday', date: '1989-11-30' lucky_duck_id: 1 }, ... } }

slide-66
SLIDE 66
  • ccasions: [1, 2, 3]

lucky_duck_id: 1 state: { luckyDucks: { 1: { id: 1, nickname: 'Sally', }, ... },

  • ccasions: {

1: { id: 1, type: 'birthday', date: '1989-11-30' }, ... } }

slide-67
SLIDE 67

Modal Back Router Hack Modal Back Router Hack

slide-68
SLIDE 68
slide-69
SLIDE 69

Demo Demo

Still no live coding

slide-70
SLIDE 70

created() { const unregisterRouterGuard = this.$router.beforeEach((to, from, next) => { this.back() next(false) }) this.$once('hook:destroyed', () => { unregisterRouterGuard() }) }, methods: { back() { //... } }

slide-71
SLIDE 71

created() { }, const unregisterRouterGuard = this.$router.beforeEach((to, from, next) => { this.back() next(false) }) this.$once('hook:destroyed', () => { unregisterRouterGuard() }) methods: { back() { //... } }

slide-72
SLIDE 72

const unregisterRouterGuard = this.$router.beforeEach((to, from, next) => { }) created() { this.back() next(false) this.$once('hook:destroyed', () => { unregisterRouterGuard() }) }, methods: { back() { //... } }

slide-73
SLIDE 73

this.back() back() { //... } created() { const unregisterRouterGuard = this.$router.beforeEach((to, from, next) => { next(false) }) this.$once('hook:destroyed', () => { unregisterRouterGuard() }) }, methods: { }

slide-74
SLIDE 74

next(false) created() { const unregisterRouterGuard = this.$router.beforeEach((to, from, next) => { this.back() }) this.$once('hook:destroyed', () => { unregisterRouterGuard() }) }, methods: { back() { //... } }

slide-75
SLIDE 75

const unregisterRouterGuard = this.$router.beforeEach((to, from, next) => { }) created() { this.back() next(false) this.$once('hook:destroyed', () => { unregisterRouterGuard() }) }, methods: { back() { //... } }

slide-76
SLIDE 76

this.$once('hook:destroyed', () => { unregisterRouterGuard() }) created() { const unregisterRouterGuard = this.$router.beforeEach((to, from, next) => { this.back() next(false) }) }, methods: { back() { //... } }

slide-77
SLIDE 77

Modal Focus Management Modal Focus Management

https:developer.mozilla.org/en US/docs/Web/Accessibility/ARIA/Roles/dialog_role

slide-78
SLIDE 78

$ npm install vue-focus-lock <FocusLock> <MyModal /> </FocusLock>

slide-79
SLIDE 79

Loading Animations Loading Animations

slide-80
SLIDE 80

// resources/view/app.blade.php <app> @include('partials.loading') </app> @extends('layouts.html') @section('body') <div id="#app"> </div> @endsection

slide-81
SLIDE 81

// resources/views/partials/loading.blade.php <div style="height: 100vh; display: flex; align-items: center; justify-content: center;"> <svg role="img" style=" width: 3rem;

  • webkit-animation: loading-bounce 2s infinite;

animation: loading-bounce 2s infinite; fill: currentColor; color: #38B2AC; " viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" > <path fill-rule="evenodd" clip-rule="evenodd" d="M29.5194 46.6111H90.727V58.7306H2 </svg> </path></div>

slide-82
SLIDE 82
  • webkit-animation: loading-bounce 2s infinite;

animation: loading-bounce 2s infinite; // resources/views/partials/loading.blade.php <div style="height: 100vh; display: flex; align-items: center; justify-content: center;"> <svg role="img" style=" width: 3rem; fill: currentColor; color: #38B2AC; " viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" > <path fill-rule="evenodd" clip-rule="evenodd" d="M29.5194 46.6111H90.727V58.7306H2 </svg> </path></div>

slide-83
SLIDE 83

// resources/views/layouts/html.blade.php @keyframes loading-bounce { 0%, 25%, 50%, 75%, 100% {

  • webkit-transform: translateY(0);

transform: translateY(0); } 15% {

  • webkit-transform: translateY(-20px);

transform: translateY(-20px); } 35% {

  • webkit-transform: translateY(-12px);

transform: translateY(-12px); } } @-webkit-keyframes loading-bounce { <head> <style> ... </style> </head>

slide-84
SLIDE 84
slide-85
SLIDE 85

Keep your bundle small Keep your bundle small

slide-86
SLIDE 86

// Before import moment from 'moment' moment.utc(value).fromNow() // After import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import relativeTime from 'dayjs/plugin/relativeTime' dayjs.extend(utc) dayjs.extend(relativeTime) dayjs.utc(value).fromNow()

slide-87
SLIDE 87

import utc from 'dayjs/plugin/utc' import relativeTime from 'dayjs/plugin/relativeTime' dayjs.extend(utc) dayjs.extend(relativeTime) // Before import moment from 'moment' moment.utc(value).fromNow() // After import dayjs from 'dayjs' dayjs.utc(value).fromNow()

slide-88
SLIDE 88

😩

677 KB (minied) 416 KB (261 KB!)

slide-89
SLIDE 89

// Before window._ = require('lodash') _.keyBy(...) // After import keyBy from 'lodash/keyBy' keyBy(...)

slide-90
SLIDE 90

Alternatives Alternatives Server rendered apps are still awesome! Inertia.js Livewire

inertiajs.com laravellivewire.com

slide-91
SLIDE 91

giftyduck.com giftyduck.com

slide-92
SLIDE 92

@jessarchercodes