Proven patterns for building Vue apps

Please fill out this quick survey!

https://chrisfritz.typeform.com/to/zH6KXA

Our backgrounds

What's our experience?

Who are you?

What will we cover?

  • Languages: how JavaScript, HTML, CSS fit in a Vue app
  • Components: specific tips for building and organizing components
  • Routing: routes, views, layouts and how they fit together to solve common problems
  • State management: how to store and access shared state in your application

...and maybe

  • Testing: making tests as painless and useful as possible
    • End-to-end tests (with Cypress)
    • Unit tests (with Jest)
  • Enabling best practices: how to avoid arguments and increase productivity by making it easy to do the right thing

Format

  1. Learn
  2. Question
  3. Apply

explanations

examples

stories

clarifications

what-ifs

code

experiment

one-on-one help

Resources

Vue Enterprise Boilerplate

please clone it now

Your Projects!

Participation tips

Raise your hand for questions at any time!

All examples are public

(no need to copy down code examples)

Please no recording

(for the privacy of participants)

Questions

Languages

Single-file components

<!-- MyComponent.vue -->

<script>
export default {
  // ...
}
</script>

<template>
  <!-- ... -->
</template>

<style>
/* ... */
</style>

JavaScript

Drop IE support if you can!

(you can save up to ~15KB gzipped)

HTML

Templates are fully declarative

(typically only 1 right way to do something)

Render functions are more flexible

HTML

functional components

Stateless and instance-less

(no data, computed, etc and no lifecycle)

<template functional>
  <!-- ... -->
</template>
export default {
  functional: true,
  // ...
  render (h, context) {
    // ...
  }
}

or

HTML

functional components

Improve performance

(especially for components rendered many times, e.g. with a v-for)

Can return multiple root nodes

boilerplate: src/components/nav-bar-routes.vue

CSS

Global CSS (usually) only in a root app.vue

(especially for components rendered many times, e.g. with a v-for)

Scope all component CSS

(using either the scoped or module attributes)

CSS

scoping

<style scoped>
/* ... */
</style>
<style module>
/* ... */
</style>

CSS

preprocessors

<style lang="scss">
.my-class {
  & > .another-class {
    @extend %placeholder-class;

    &:hover {
      // ...
    }
  }
}
</style>

Questions

Practice

In the boilerplate

In your app

Components

Order of elements in SFCs

<!-- MyComponent.vue -->

<script>
export default {
  // ...
}
</script>

<template>
  <!-- ... -->
</template>

<style>
/* ... */
</style>

Always use SFCs

Simpler refactoring

easy to add a <template> or <style> later

Automatic names

Vue Loader provides fallback names that Vue looks for internally

Useful patterns

in the Style Guide

Wrap vendor components

More easily switch to an alternative

Control the interface

boilerplate: src/components/_base-icon.vue

Component organization

flat vs nested

Flat makes refactors easier

no need to update imports after moving a component

Flat makes finding files easier

folder scoping leads to lazily named files, because they don't have to be unique

Transparent components

Use component element wrappers just like normal elements

boilerplate: src/components/_base-input.vue

Questions

Practice

In the boilerplate

  • Create a new base component, then use that component in another one without explicitly importing it

  • Create a <BaseSelect> component that acts as a transparent wrapper for a <select> element.

In your app

Routing

with Vue Router

Routing overview

Routes

mapping paths to view components

Layout components

markup shared between pages (header, navbar, sidebar, etc)

View components

definitions for page-level components

View components

Can use in-component route guards

beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave

(often better to use route-level guards instead)

Can access $route or accept props from params

(other components can also access $route, but usually shouldn't)

Simplifying views

...but slightly slower page renders

can often use <keep-alive> with include if it's a problem

<router-view :key="$route.fullPath"/>

no more beforeRouteUpdate!

Meta info for views

Page <title>, <meta name="description">, etc

export default {
  metaInfo() {
    return {
      title: this.user.name,
      meta: [
        {
          name: 'description',
          content: `The user profile for ${this.user.name}.`,
        },
      ],
    }
  },
  // ...
}

Layouts

<script>
import NavBar from '@components/nav-bar'

export default {
  components: { NavBar },
}
</script>

<template>
  <div :class="$style.container">
    <NavBar/>
    <slot/>
  </div>
</template>

<style lang="scss" module>
@import '~@design';

.container {
  min-width: $size-content-width-min;
  max-width: $size-content-width-max;
  margin: 0 auto;
}
</style>

View component renders here

Using a layout in a view

<script>
import Layout from '@layouts/main'

export default {
  components: { Layout },
}
</script>

<template>
  <Layout>
    <h1>Home Page</h1>
    <img
      src="@assets/images/logo.png"
      alt="Logo"
    >
  </Layout>
</template>

Layout wraps the view's template

Layout tips

Even in layouts, avoid global CSS

keep element selectors in App.vue or nested under a scoped class

Nested view components might not have a layout

e.g. for a tabbed interface in a dashboard

Route definitions

import store from '@state/store'

export default [
  {
    path: '/',
    name: 'home',
    component: require('@views/home').default,
  },
  {
    path: '/login',
    name: 'login',
    component: require('@views/login').default,
    beforeEnter(routeTo, routeFrom, next) {
      // If the user is already logged in
      if (store.getters['auth/loggedIn']) {
        // Redirect to the home page instead
        next({ name: 'home' })
      } else {
        // Continue to the login page
        next()
      }
    },
  },
  // ...
}

Inline require

In-route guards keep the views simple

Access the store

Animate page loads

(especially when using in-route guards to fetch data)

import NProgress from 'nprogress/nprogress'

const router = new VueRouter({ /* ... */ })

// After navigation is confirmed, but before resolving...
router.beforeResolve((routeTo, routeFrom, next) => {
  // If this isn't an initial page load...
  if (routeFrom.name) {
    // Start the route progress bar.
    NProgress.start()
  }
  next()
})

// When each route is finished evaluating...
router.afterEach((routeTo, routeFrom) => {
  // Complete the animation of the route progress bar.
  NProgress.done()
})

Note: branded is better!

Handling 404s

<script>
import Layout from '@layouts/main'

export default {
  components: { Layout },
  props: {
    resource: {
      type: String,
      default: '',
    },
  },
}
</script>

<template>
  <Layout>
    <h1>
      404
      <span v-if="resource">
        {{ resource }}
      </span>
      Not Found
    </h1>
  </Layout>
</template>
{
  path: '/404',
  name: '404',
  component: require('@views/404').default,
  props: true,
},
{
  path: '*',
  redirect: '404',
},
next({ name: '404', params: { resource: 'User' } })

1

2

3

Lazy-loaded routes

{
  path: '/admin',
  name: 'admin-dashboard',
  component: () => import('@views/admin'),
}
{
  path: '/admin',
  name: 'admin-dashboard',
  component: require('@views/admin').default,
}

Questions

Practice

In the boilerplate

In your app

State management

with Vuex

Vuex store: core concepts

state

~== data properties

getters

~== computed properties

mutations

synchronous functions

that update state

 

store.commit('COMMIT_NAME')

actions

~== methods

 

store.dispatch('actionName')

Vuex modules

const store = new Vuex.Store({
  modules: {
    todos: {
      state: {
        all: [],
      },
      getters: {
        completedTodos(state) {
          return state.all.filter(todo =>
            todo.isComplete
          )
        },
      },
      mutations: {
        PUSH_TODO(state, newTodo) {
          state.all.push(newTodo)
        },
      },
      actions: {
        addTodo({ commit }, newTodo) {
          commit('PUSH_TODO', newTodo)
        },
      },
    },
  },
})

store.getters.completedTodos

store.state.todos.all

store.commit('PUSH_TODO')

store.dispatch('addTodo')

Namespaced modules

const store = new Vuex.Store({
  modules: {
    todos: {
      namespaced: true,
      state: {
        all: [],
      },
      getters: {
        completed(state) {
          return state.all.filter(todo =>
            todo.isComplete
          )
        },
      },
      mutations: {
        PUSH(state, newTodo) {
          state.all.push(newTodo)
        },
      },
      actions: {
        add({ commit }, newTodo) {
          commit('PUSH', newTodo)
        },
      },
    },
  },
})

store.getters['todos/completed']

store.state.todos.all

store.commit('todos/PUSH')

store.dispatch('todos/add')

Module files

export const namespaced = true

export const state = {
  all: [],
}

export const getters = {
  completed(state) {
    return state.all.filter(todo => todo.isComplete)
  },
}

export const mutations = {
  PUSH(state, newTodo) {
    state.all.push(newTodo)
  },
}

export const actions = {
  add({ commit }, newTodo) {
    commit('PUSH', newTodo)
  },
}

Multiple named exports

allows flexible organization

Automatic Vuex module registration

import Vue from 'vue'
import Vuex from 'vuex'
import modules from './modules'

Vue.use(Vuex)

const store = new Vuex.Store({ modules })

Custom module defaults

const store = new Vuex.Store({ modules })

// Automatically run the `init` action for
// every module, if one exists.
for (const moduleName of Object.keys(modules)) {
  if (modules[moduleName].actions.init) {
    store.dispatch(`${moduleName}/init`)
  }
}
modules[modulePath.pop()] = {
  // Modules are namespaced by default
  namespaced: true,
  ...requireModule(fileName),
}

boilerplate:

src/state/store.js

Grouped helpers

import { mapState, mapGetters, mapActions } from 'vuex'

export const authComputed = {
  ...mapState('auth', {
    currentUser: state => state.currentUser,
  }),
  ...mapGetters('auth', ['loggedIn']),
}

export const authMethods = mapActions('auth', 
  ['logIn', 'logOut']
)

boilerplate: src/state/helpers.js

Using grouped helpers

import { 
  authComputed, 
  authActions 
} from '@state/helpers'

export default {
  computed: {
    ...authComputed,
  },
  methods: {
    ...authActions,
  },
}

Questions

Practice

In the boilerplate

  1. Create a new shoppingList module to store shopping list items.

  2. Create helpers for the shoppingList module in src/state/helpers.js.

  3. In src/router/views/home.vue, import some state helpers to display, add, and remove items from the shopping list.

In your app

  • Add Vuex to your app with a single shoppingList module, as described to the left.

  • Automatically register all your Vuex modules.

  • Refactor a module to be namespaced, updating all references to that module.

  • Automatically call an init action for your modules.

Testing

Biggest problems

we stop writing and running tests when they're NOT

Visible

Everyone knows when

tests are failing

Reliable

Tests don't fail intermittently

Fast

Tests run quickly

Debuggable

When a test fails,

it's easy to learn why

End-to-end tests

with Cypress

Why Cypress?

Writing e2e tests

Best practices

Don't maintain state between tests

they should be able to run with it.only

Don't select elements with classes

think from the user's perspective, or select elements by intent

Integrating e2e tests

Integrate into your review process

with continuous integration

Write and run in development

yarn dev:e2e in the boilerplate

Questions

Practice

In the boilerplate

  • Write a new e2e test for one of the features you wrote in a previous practice round.

  • Develop a small new feature using test-driven development with yarn dev:e2e. That means:

    1. First write the test

    2. Then develop the feature

In your app

Unit tests

with Vue Test Utils

and Jest

Why Vue Test Utils?

(currently in beta)

Mocks/stubs Vue components and properties

to full test a component's interface

Works with any test framework

Versatile rendering

including shallow rendering, to avoid testing subcomponents

Why Jest?

Great output on failure

Why Jest?

Quite fast

Why not Jest?

Not a real browser

uses JSDOM to simulate the browser

Only ~70% support .vue files

CSS only experimentally supported

Have to reinvent your Webpack config

aliases, dynamic imports, etc require additional config

Writing unit tests

Don't test that Vue works

e.g. asserting that a data, computed, etc property exists

Build unit tests into generators

it's easier to add a new test when the file already exists

Pull out (potentially) shared logic

big components often scare people, so they go untested

Usually use a shallow render

otherwise, a problem in a common component can breaks many tests

Integrating unit tests

Never commit failing tests

run unit tests against staged files on a pre-commit hook

Continuous integration

make sure you know when tests are failing

Solve hard problems in setup files

create helpers and custom matchers to simplify complex assertions

First-class unit tests

Put unit tests next to tested file

(e.g. hello.vue and hello.unit.js)

 

  • never think about organization of unit tests
  • missing tests are very obvious
  • helps onboard new developers, since they can see exactly what the file is supposed to do

Questions

Practice

In the boilerplate

  • Make a breaking change to a component and try to commit.
  • Make a change to a file that causes its test to fail. Now fix the test.
  • Use test-driven development on an existing component, first writing a test for a new feature, then the adding the feature.

In your app

  • Refactor an existing component test to use Vue Test Utils (try the shallow render).

  • Delete a test that confirms Vue works.

  • Refactor your unit tests to be side-by-side with your source files.

Making it easy to follow best practices

Great recruiting tool!

people get excited when they see a great architecture laid out

Replace (some) training with doc PRs

when trainees review doc PRs, training is made reusable

module.exports = {
  '*.{js,jsx}': [
    'eslint --fix',
    'prettier --write',
    'git add',
    'jest --bail --findRelatedTests',
  ],
  '*.json': ['prettier --write', 'git add'],
  '*.vue': [
    'eslint --fix',
    'stylelint --fix',
    'prettier --write',
    'git add',
    'jest --no-cache --bail --findRelatedTests',
  ],
  '*.scss': ['stylelint --fix', 'prettier --write', 'git add'],
  '*.md': ['markdownlint', 'prettier --write', 'git add'],
  '*.{png,jpeg,jpg,gif,svg}': ['imagemin-lint-staged', 'git add'],
}

Reduce friction

speed up development and simplify refactors

Enforce best practices

e.g. automatically generate a unit test with each module

Custom snippets

with Vetur

Same advantages as generators

Soon...

next version of Vetur will contain a new option:

"vetur.completion.useScaffoldSnippets": false

Can be combined with generators for automatic basic tests for components, views, utils, etc.

Meta validations

Warn other devs when they might be using a component wrong

 

(when a prop's validator isn't enough)

Can even validate $listeners or $attrs

Global validation mixin

(only in development)

if (process.env.NODE_ENV !== 'production') {
  Vue.mixin({
    // Use lifecycle hooks to scan components for 
    // patterns that should be avoided.
  })
}

Questions

Practice

In the boilerplate

  • Save a file with a syntax or linting error, then try to commit the file.
  • Add a new Hygen generator for creating doc pages.
  • Create a new Jest matcher to assert that a component is functional. Try it on the nav-bar-routes.vue component.

In your app

  • Add one linter to your app, run with a lint command in package.json.

  • Use lint-staged to either lint or run unit tests against staged files (or both).

  • Add a meta validation to a component that your team has previously misused.

That's all I know

Open practice time!

Proven patterns for building Vue apps

By Chris Fritz

Proven patterns for building Vue apps

The MIT License (MIT) Copyright (c) 2018-present Chris Fritz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

  • 5,346

More from Chris Fritz