Claudio Bisconti
mail: claudio.bisconti@comm-it.it
linkedin: linkedin.com/in/claudiobisconti
github: github.com/cbspire
About me
- Sales Account
- Technical Leader
- Fullstack software developer
@
What I know
PREVIOUSLY ON
Vue.js
Created by Evan You (Ex Google)
First release in February 2014
Actually used by
- Laravel
- Alibaba
- Xiaomi
- Netflix
- Gitlab
- Facebook 🤫
- and more others
Vue.js
/vjuː/ => { 'view' }
- Progressive framework
- for building user interfaces
- Modern and lightweight approach
- Components oriented
- with declarative rendering
- and reactivity approach
- where models are simple Javascript object
Vue.js
<template>
<div id="my-app">
<header-bar :user="username"/>
<app-loader v-if="loading" />
</custom-content v-else />
</div>
</template>
<script>
import { HeaderBar, AppLoader, CustomContent } from '../components'
export default {
name: 'MyApp',
components: { HeaderBar, AppLoader, CustomContent }
data() {
return {
loading: true,
user: {}
}
},
computed: {
username() {
return `${this.firstname} ${this.lastname}`
}
},
async mounted() {
this.user = await fetchUser()
}
}
</script>
<style>
#my-app {
width: 100vw; height: 100vh;
}
</style>
Framework comparison
Angular | React | Vue | jQuery | |
---|---|---|---|---|
Type | Framework | Library | Framework | Library |
Building SPA | Y | Y | Y | 🧐 |
Using in already made applications | 😰 | Y | Y | 💪 |
DOM Manipulation | Y | Y | Y | Y |
Virtual DOM | Y | Y | Y | N |
Production build size | ~300K | ~100K | ~30K | ~85K |
Reactive components | Y | Y | Y | 😟 |
Router inside | Y | N | N | ? |
HTML Templates | Y | N | Y | Y |
JSX | N | Y | Y | N |
Dependency Tracking | Manual | Manual | Auto | 🙈 |
Learning curve | Fast | Medium | Fast | Auto |
U mixins
Mixins are a flexible way to distribute reusable functionalities for Vue components
A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be “mixed” into the component’s own options.
U mixins
mixin example
const themeMixin = {
props: {
light: {
type: Boolean,
default: true
},
dark: {
type: Boolean,
default: false
}
}
computed: {
computedThemeClasses() {
return {
['theme-' + this.light]: this.light,
['theme-' + this.dark]: this.dark,
}
}
}
}
export default themeMixin
U mixins
mixin use example
<template>
<div :class="computedThemeClasses">
...
</div>
</template>
<script>
import themeMixin from '../mixins';
export default {
name: 'MyComponent',
mixins: [themeMixin]
}
</script>
U mixins
global mixin
Vue.mixin({
created: function () {
var theme = this.$options.theme
if (theme) {
...
}
}
})
new Vue({
theme: 'dark'
})
Once you apply a mixin globally, it will affect every Vue instance created afterwards. When used properly, this can be used to inject processing logic for custom options:
U mixins
merge options
Vue.config.optionMergeStrategies.theme = (toVal, fromVal) => {
const allowedThemes = ['default', 'light', 'dark'];
if (!allowedThemes.includes(toVal)) {
return 'default';
}
return toVal;
}
Vue router
Official router
Creating a Single-page Application with Vue + Vue Router is dead simple.
Vue router
creating routes
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import LoginPage from './pages/Login'
import DashboardPage from './pages/Dashboard'
const UsersList = () => import('./pages/Users')
const ProjectsList = () => import(/* webpackChunkName: "page-projects" */ './pages/Projects')
const routes = [
{ path: '/login', component: LoginPage },
{ path: '/dashboard', component: Dashboard },
{ path: '/users', component: UsersList },
{ path: '/projects', component: ProjectsList }
]
const router = new VueRouter({
routes // short for `routes: routes`
})
new Vue({
//...
router
})
Vue router
routes
const routes = [
{
path: '/user/:id',
component: UserPage,
children: [
{
path: '', /* path: /users/:id: */
component: UserHomePage,
},
{
path: 'profile', /* path: /users/:id:/profile */
component: UserProfilePage,
name: 'user.profile',
props: true
},
{
path: 'notifications', /* path: /users/:id:/notifications */
component: UserNotificationsPage,
children: [
{
path: ':nid', /* path: /users/:id:/notifications/:nid */
component: UserNotificationPage
}
]
}
]
},
]
Vue router
<router-view></router-view>
<div id="app">
<h1>Hello App!</h1>
<p>
<router-link to="/users">Users</router-link>
<router-link to="/dashboard">Dashboard</router-link>
</p>
<!-- component matched by the route will render here -->
<router-view></router-view>
</div>
Vue router
<router-link></router-link>
<router-link to="/users">Users</router-link>
Rendered as: <a href="...">Users</a>
<router-link tag="button" to="/dashboard">Dashboard</router-link>
Rendered as: <button>Dashboard</button>
<router-link :to="{ name: 'user.profile', params: { id: 1 }}">
User #1
</router-link>
Vue router
Js usage
<template>
<div>
{{ $route.name }}
{{ $route.params.id }} === {{ id }}
</div>
</template>
<script>
export default {
name: 'UserProfile',
props: ['id'],
mounted() {
let userId = this.$route.params.id
// or use this.id
// fetchUserProfileData ?
},
methods: {
onNotificationTabClicked(nid) {
this.$router.push({
name: 'user.notification.single',
params: { nid }
})
}
}
}
</script>
Vue router
Named views
<template>
<div id="myapp">
<header>
<router-view name="header"></router-view>
</header>
<router-view></router-view>
</div>
</template>
const routes = [
{
path: '/user/:id',
components: {
default: UserPage,
header: UserPageHeader
}
}
]
Vue router
Named views
<template>
<div id="myapp">
<header>
<router-view name="header">
</router-view>
</header>
<router-view></router-view>
</div>
</template>
const routes = [
{
path: '/user/:id',
components: {
default: UserPage,
header: UserPageHeader
}
}
]
Vue router
global/route navigation guards
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
to = { // https://router.vuejs.org/api/#the-route-object
path,
params
query,
hash,
fullPath,
matched,
...
}
next()
})
router.beforeEnter((to, from, next) => {
...
next()
})
Vue router
component navigation guards
export default {
template: `...`,
beforeRouteEnter (to, from, next) {
// called before the route that renders this component is confirmed.
// does NOT have access to `this` component instance,
// because it has not been created yet when this guard is called!
},
beforeRouteUpdate (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params `/foo/:id`, when we
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
// will be reused, and this hook will be called when that happens.
// has access to `this` component instance.
},
beforeRouteLeave (to, from, next) {
// called when the route that renders this component is about to
// be navigated away from.
// has access to `this` component instance.
}
}
Vue router
router transitions
<transition name="fade">
<router-view></router-view>
</transition>
export default {
watch: {
'$route' (to, from) {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
}
}
}
<transition :name="transitionName">
<router-view></router-view>
</transition>
dynamic router transitions
Vue router
other features
- linkActiveClass
- scrollBehaviours
- parseQuery / stringifyQuery
- fallback
- currentRoute
- etc etc
Vuex
Getting started
At the center of every Vuex application is the store. A "store" is basically a container that holds your application state. There are two things that make a Vuex store different from a plain global object
- Vuex stores are reactive
- You cannot directly mutate the store's state.
Vuex
Getting started
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Vuex
State
const store = new Vuex.Store({
state: {
count: 0,
users: [],
user: {
id: '',
...
},
auth: {
expiredAt: null
token: null
}
}
})
- Initial state
- All state objects
Reactive... but only first level
Vuex
Actions
const store = new Vuex.Store({
...
actions: {
increment (context) {
context.commit('increment')
},
decrement ({commit, dispatch, store, rootStore}) {
if (store.count > 0) {
commit('increment')
}
},
setCount ({commit}, payload) {
commit('setCount' parseInt(payload.count))
},
setCount2 ({commit}, {count}) {
commit('setCount' parseInt(count))
},
setAsyncCount ({commit}, {count}) {
return new Promise((resolve, reject) => {
api.setCount(count).then(() => {
commit('setCount' parseInt(count))
resolve()
).catch((error) => {
reject(error.message)
})
})
}
}
})
Vuex
Actions inside Components
import { mapActions } from 'vuex'
export default {
name: 'MyComponent',
methods: {
...mapActions([
'increment',
'setCount' /* map `this.setCount(count)` */
])
},
mounted() {
this.$store.dispatch('increment')
this.$store.dispatch('setCount', 50)
this.$store.dispatch('setAsyncCount', 50)
.then(() => {
...
}).catch(err => {
alert(err)
})
}
}
Vuex
Mutations
const store = new Vuex.Store({
...
mutations: {
increment (state) {
state.count++
},
decrement (state) {
state.count--
},
setCount (state, count) {
state.count = count
}
}
})
The only way to actually change state in a Vuex store is by committing a mutation. Vuex mutations are very similar to events: each mutation has a string type and a handler.
Vuex
Mutations inside Components
import { mapMutations } from 'vuex'
export default {
name: 'MyComponent',
methods: {
...mapMutations([
'increment',
'setCount' /* map `this.setCount(count)` */
])
},
mounted() {
this.$store.commit('increment')
this.$store.commit('setCount', 50)
}
}
Vuex
Accessing store from Components
export default {
name: 'MyComponent',
computed: {
getCount () {
return this.$store.state.count
}
}
}
Vuex
Getters
const store = new Vuex.Store({
...
getters: {
count: state => state.count,
isEmpty : state => {
return state.count > 0
},
allNotifications: state => state.notifications,
notificationsCount: state => state.notifications.length,
unreadNotifications: state => {
return _.filter(state.notifications, {'active', true})
},
readNotifications: state => {
return _.filter(state.notifications, {'active', false})
},
blueNotifications: state => {
return _.filter(state.notifications, {'blue', true})
}
}
})
Vuex
Getters inside Components
import { mapGetters } from 'vuex'
export default {
name: 'MyComponent',
computed: {
...mapGetters([
'count',
'isEmpty',
'unreadNotifications'
]),
formatNotifications() {
return _.map(this.unreadNotifications, (notification) => {
return {
id: notification._id,
title: notification.name,
date: this.$moment(notification.ts).format('LLLL')
}
});
}
}
}
Vuex
Vue DevTools integration
Beautiful free course
Events communication
<template>
<div class="my-custom-list">
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
<button @click="selectItem(item)">Select</button>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'MyCustomList',
props: ['items']
methods: {
selectItem(item) {
this.$emit('itemSelected', item)
}
}
}
</script>
Events communication
<template>
<my-custom-list :list="items" @item-selected="onItemSelected" />
</template>
<script>
export default {
name: 'MyWrapper',
data() {
return {
items: []
}
},
methods: {
onItemSelected(item) {
...
this.$parent.$parent.itemSelected(item)
/* 😱😨😰😓🤔🤫😈☠😡 */
}
}
}
</script>
Events communication
Component communication pattern
import Vue from 'vue'
export const eventHub = new Vue();
First
Events communication
Component communication pattern
import eventHub from './eventhub'
export default {
...
methods: {
action(items) {
eventHub.$emit('dispatch-data', items)
}
}
}
Then
Events communication
Component communication pattern
import eventHub from './eventhub'
export default {
...
mounted() {
eventHub.$on('dispatch-data', (items) => {
this.items = items
})
}
}
Then
Events communication
v-model
<template>
<my-custom-item v-model="model" />
</template>
<script>
export default {
name: 'MyWrapper',
data() {
return {
model: 'test'
}
}
}
</script>
Events communication
v-model inside
export default {
name: 'MyCustomItem',
props: {
value: {
type: String,
default: ''
},
},
data() {
return {
myValue: ''
}
},
mounted() {
this.myValue = this.value
},
methods: {
onValueChanged(newValue) {
this.$emit('input', newValue)
}
},
watch: {
value: (newVal, oldVal) {
if (newVal !== oldVal) {
this.$emit('input', newValue)
}
}
}
}
Vue.*
how to extend Vue?
With Plugins!
- Add some global properties or methods
- Add global directives, filters, transitions, etc...
- U mixins
- Vue instance properties this.$something
- Add libraries
Vue plugins
import MyDirective from './directives/myDirective'
import themeMixin from './mixins/themeMixin'
import MyCustomList from './components/MyCustomList'
const MyPlugin = {}
MyPlugin.install = function (Vue, options) {
Vue.myGlobalMethod = function () {
// something logic ...
}
Vue.directive('my-directive', MyDirective)
Vue.component('my-custom-list', MyCustomList)
Vue.mixin(themeMixin)
Vue.prototype.$myMethod = function (methodOptions) {
// something logic ...
}
}
export default MyPlugin
Vue plugins
import MyPlugin from './plugins/myPlugin'
Vue.use(MyPlugin)
Vue.use(MyPlugin, { ... options })
That's all
Vue Testing
Vue test utils
There are many popular JavaScript test runners, and Vue Test Utils works with all of them. It's test runner agnostic.
Vue Testing
Documentation example
// counter.js
export default {
template: `
<div>
<span class="count">{{ count }}</span>
<button @click="increment">Increment</button>
</div>
`,
data () {
return {
count: 0
}
},
methods: {
increment () {
this.count++
}
}
}
Vue Testing
Documentation example
// test.js
// Import the `mount()` method from the test utils
// and the component you want to test
import { mount } from '@vue/test-utils'
import Counter from './counter'
// Now mount the component and you have the wrapper
const wrapper = mount(Counter)
// You can access the actual Vue instance via `wrapper.vm`
const vm = wrapper.vm
// To inspect the wrapper deeper just log it to the console
// and your adventure with the Vue Test Utils begins
console.log(wrapper)
Vue Testing
Documentation example
import { mount } from '@vue/test-utils'
import Counter from './counter'
describe('Counter', () => {
// Now mount the component and you have the wrapper
const wrapper = mount(Counter)
it('renders the correct markup', () => {
expect(wrapper.html()).toContain('<span class="count">0</span>')
})
// it's also easy to check for the existence of elements
it('has a button', () => {
expect(wrapper.contains('button')).toBe(true)
})
it('button click should increment the count', () => {
expect(wrapper.vm.count).toBe(0)
const button = wrapper.find('button')
button.trigger('click')
expect(wrapper.vm.count).toBe(1)
})
})
Vue Testing
Documentation example - Shallow Rendering
import { shallowMount } from '@vue/test-utils'
import Counter from './counter'
const wrapper = shallowMount(Counter)
wrapper.vm // the mounted Vue instance
wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)
expect(wrapper.emitted().foo).toBeTruthy()
expect(wrapper.emitted().foo.length).toBe(2)
expect(wrapper.emitted().foo[1]).toEqual([123])
wrapper.setData({ count: 10 })
wrapper.setProps({ foo: 'bar' })
wrapper.trigger('click')
wrapper.find('button').trigger('click')
Vue Testing
Documentation example - Create Local Vue
import { createLocalVue } from '@vue/test-utils'
// create an extended `Vue` constructor
const localVue = createLocalVue()
// install plugins as normal
localVue.use(MyPlugin)
// pass the `localVue` to the mount options
mount(Component, {
localVue
})
Large scale applications
-
lang
-
mocks
-
modules
-
<folder>
-
components
-
routes
-
styles
-
pages
-
-
-
plugins
-
router
-
routes
-
-
store
-
modules
-
Large scale applications
store modules
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// extract js files inside modules folder
const requireModule = require.context('./modules', true, /index\.js$/)
const modules = {}
requireModule.keys().forEach(fileName => {
const moduleName = fileName.replace(/(\.\/|\/index\.js)/g, '')
modules[moduleName] = requireModule(fileName).default
})
const store = new Vuex.Store({
modules
})
export default store
Large scale applications
router modules
Cordova
Cordova
Start!
vueConfig.pages = {
index: {
entry: 'src/main.js',
template: 'public/index.html',
filename: 'index.html',
title: 'MyApplication',
chunks: ['chunk-vendors', 'chunk-common', 'index']
}
}
vueConfig.outputDir = 'www'
Cordova
my beautiful cordovaLoader.js
const cordovaLoader = (cb) => {
appendCordovaScript();
document.addEventListener('deviceready', () => {
cb();
setTimeout(() => {
navigator.splashscreen.hide();
}, 200)
})
}
const appendCordovaScript = () => {
const hasAlready = document.URL.indexOf('http') !== 0 || window.hasOwnProperty('cordova');
if (hasAlready) {
return;
}
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = getCordovaUrl();
document.head.appendChild(script);
}
const getCordovaUrl = () => {
return 'cordova.js';
}
export default cordovaLoader
Cordova
....
import cordovaLoader from './cordovaLoader'
cordovaLoader(() => {
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
});
vueConfig.pages = {
index: {
entry: 'src/main.mobile.js',
template: 'public/index.html',
filename: 'index.html',
title: 'MyApplication',
chunks: ['chunk-vendors', 'chunk-common', 'index']
}
}
vueConfig.outputDir = 'www'
Awesome Vue. Vol.2
By Claudio Bisconti
Awesome Vue. Vol.2
- 1,205