Proven patterns for building Vue apps
Please fill out this quick survey!
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
- Learn
- Question
- 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
-
Add a <label> to the _base-input.vue component, then style that label with a CSS module class.
-
Refactor the _base-button.vue component to use a render function instead of a template. Then make it a functional component.
In your app
- Refactor a template to use a render function instead.
- Refactor a component to use CSS modules for scoping.
- Refactor an existing component that’s rendered many times (e.g. with a v-for) to a functional component.
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
-
Add a prefix (e.g. "base-") to a component, then create a _globals.js file to import all the .vue files with that prefix
-
Refactor a component that wraps an element to be fully transparent
-
Refactor a single-instance component to use the "the-" prefix.
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
-
Add a new layout and use it inside an existing view component.
-
Add an /about page, with a path in routes.js pointing to a new about.vue view.
-
Make one route lazy.
In your app
-
Integrate Vue Router into your app, with a single “main” layout component and a single “home” view.
-
Use <router-view :key="$route.fullPath"> to simplify a view component.
-
Add vue-meta and specify metaInfo for a view component.
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 })
boilerplate: src/state/modules/index.js
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:
boilerplate:
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
-
Create a new shoppingList module to store shopping list items.
-
Create helpers for the shoppingList module in src/state/helpers.js.
-
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 manipulate the DOM for state setup
manually manipulate your app instead
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
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:
-
First write the test
-
Then develop the feature
-
In your app
-
Follow these instructions to integrate Cypress into your app.
-
Refactor a test targeting classes to use content or data-testid instead.
-
Set up Circle CI for your app, including e2e tests.
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
Pre-commit guards
(with lint-staged)
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'],
}
Build generators
with Hygen
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
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.
- 8,445