Building a modular application with VueJS

Yong Jun

16 August 2017

Background

Credits:

Chi-Loong from V/R

SchoolPicker.sg

Why we chose VueJS?

Developer Happiness

=)

  • Approachable
  • Declarative syntax
  • Component-based architecture
  • Single file components (.vue)
  • Supporting libraries (vuex, vue-router, vue-loader, vue-devtool)
  • Well written documentation

Our reasons

What others are saying

  • Blazing fast
  • Small runtime
  • Minimal optimization
<template>
  <div class="my-component">
    <h1>{{title}}</h1>
    <p>{{content}}</p>
    <SubComponent />
  </div>
</template>

<script>
import SubComponent from './SubComponent'

export default {
  data () {
    return {
      title: "Hello world",
      content: "My simple vue app"
    }
  },
  components: {SubComponent}
}
</script>

<style lang="scss" scoped>
.my-component {
  h1 {
    text-align: center;
  }
}
</style>

Single file component (.vue)

  • HTML <template>, CSS <style>, JS <script> in one file
  • Looks like a Polymer web component, functions more like React's JSX
  • Compiled into a Javascript bundle using Webpack (vue-loader) or Browserify (vueify)
  • CSS scoping out-of-the-box
  • Can be registered globally or imported locally

React vs Vue

Some comparison

Class vs Plain JS Object

// React JSX

import React from 'react'
import PropTypes from 'prop-types'

export default class MyComponent extends React.Component {
  constructor () {
    this.state = {
      count: 0
    }
    this.increment = this.increment.bind(this)
  }

  increment () {
    this.setState({count: this.state.count + 1})
  }

  render () {
    return (
      <div>
        <h1>Hi {this.props.user}</h1>
        <p>{this.state.count}</p>
        <button onClick={this.increment}>Click me</button>
      </div>
    )
  }
}

MyComponent.propTypes = {
  user: PropTypes.string.isRequired
}
// Vue component in pure JS

export default {
  template: `
    <div>
      <h1>Hi {{user}}</h1>
      <p>{{count}}</p>
      <button v-on:click="increment">Click me</button>
    </div>
  `,
  props: {
    user: String
  },
  data () {
    return {
      count: 0
    }
  },
  methods: {
    increment () {
      this.count++
    }
  }
}

render ( ) function vs computed

// React

export default function (props) {
  const lengthMM = props.length.toFixed() + ' mm'
  const lengthInches = (props.length / 25.6).toFixed() + ' inches'
  
  return <p>{lengthMM} = {lengthInches}</p>
}
// Vue

export default {
  template: '<p>{{lengthMM}} = {{lengthInches}}</p>',
  props: ['length']
  computed: {
    lengthMM () {
      return this.length.toFixed() + ' mm'
    },
    lengthInches () {
      return (this.length / 25.6).toFixed() + ' inches'
    }
  }
}

Callback functions vs Emitting events

<!-- React -->

<!-- Parent -->
<div>
  <Child
    count={this.state.count}
    increment={this.increment} />
</div>

<!-- Child -->
<div>
  Count: {this.props.count}
  <button onclick={this.props.increment}>
    Click me
  </button>
</div>
<!-- Vue -->

<!-- Parent -->
<div>
  <Child
    v-bind:count="count"
    v-on:increment="increment" />
</div>

<!-- Child -->
<div>
  Count: {{count}}
  <button v-on:click="$emit('increment')">
    Click me
  </button>
</div>

Vuex vs Redux

// Vue

const vm = new Vue ({
  data: {
    count: 0
  },

  computed: {
    formattedCount () {
      return this.count + ' apples'
    }
  },

  methods: {
    increment () {
      this.count++
    },

    incrementAsync () {
      setTimeout(this.increment, 1000)
    }
  }
}

vm.increment()
vm.incrementAsync()
// Vuex

const store = new Vuex.Store({
  state: {
    count: 0
  },

  getters: {
    formattedCount (state) {
      return state.count + ' apples'
    }
  },

  mutations: {
    increment (state) {
      state.count++
    }
  },

  actions: {
    incrementAsync (context) {
      setTimeout(
        () => context.commit('increment'),
        1000
      )
    }
  }
})

store.commit('increment')
store.dispatch('incrementAsync')

vue-devtool

How we applied Vue's modularity and composability in building SchoolPicker.sg

Everything is built up from components

Card-based UI

Directory Organization

Versioning with global constants

In very common design pattern seen in our app

<!-- ListView component -->

<template>
  <div class="list-view">
    <ListCard />
  </div>
</template>

<script>
import ListCardVerA from './ListCardVerA'
import ListCardVerB from './ListCardVerB'

const ListCard = process.env.VERSION === 'A'
  ? ListCardVerA
  : ListCardVerB

export default {
  components: {ListCard}
}
</script>
// In webpack.config.js

var webpack = require('webpack')

module.export = {
  // entry
  // output
  // loaders...
  
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        VERSION: JSON.stringify(process.env.VERSION),
        NODE_ENV: JSON.stringify('development')
      }
    })
  ]
}
> VERSION=A webpack
// Original code

function prod () {
  console.log('production')
}

function dev () {
  console.log('development')
}

if (process.env.NODE_ENV === 'production') {
  prod()
} else {
  dev()
}
// After DefinePlugin

...

if ('production' === 'production') {
  prod()
} else {
  dev()
}
// After UglifyJsPlugin

...

if (true) {
  prod()
} else {
  dev()
}
// Next pass

function prod () {
  console.log('production')
}

prod()

How DefinePlugin works together with UglifyJSPlugin

function prod () {
  console.log('production')
}

function dev () {
  console.log('development')
}

if (process.env.NODE_ENV === 'production') {
  prod()
} else {
  dev()
}

This works in webpack v1 as long we include DefinePlugin and

UglifyJSPlugin

import {prod, dev} from './modularCode'

if (process.env.NODE_ENV === 'production') {
  prod()
} else {
  dev()
}

This doesn't works in webpack v1 but works in webpack v2 because of tree-shaking

import prod from './productionCode'
import dev from './developmentCode'

if (process.env.NODE_ENV === 'production') {
  prod()
} else {
  dev()
}

This doesn't work in webpack v2 but might work with webpack v3 because of scope hoisting 

Note on tree-shaking

Using Vuex modules

Tips for writing in Vue

UI Framework

Tooling setup

<style lang="scss">
.parent {
  width: 100%;

  .child {
    padding: 10px;
  }
}
</style>

Enable linting on .vue file

Specifying "lang" allows correct syntax highlighting

Using v-for and v-if  within the same tag

<!-- Does this work -->

<span v-if="items" v-for="item in items">
  {{item.label}}
</span>

<!-- NO because v-for has priority over v-if -->
<!-- Do this instead -->

<template v-if="items">
  <span v-for="item in items">
    {{item.label}}
  </span>
</template>

<!-- On the other hand, this works -->

<span v-for="item in items" v-if="item.valid">
  {{item.label}}
</span>

Sass Gotcha

// App.vue

<template>
  <div>
    <Child />
  </div>
</templat>

<script>
import Child from './Child'

export default {
  components: {Child}
}
</script>

<style>
$color-primary: #HHH;

@mixin round-top($radius) {
  border-radius: $radius 0 0 $radius;
}
</style>
// Child.vue

<style lang="scss">
.child {
  color: $color-primary;
  @include $round-top(10px);
}
</style>
// variables_mixins.scss

$color-primary: #HHH;

@mixin round-top($radius) {
  border-radius: $radius 0 0 $radius;
}
// Child.vue

<style lang="scss">
@include "~style/variables_mixins.scss"

.child {
  color: $color-primary;
  @include $round-top(10px);
}
</style>

This doesn't work

This does

Vuex Gotcha

// sometime you may want to initialize a component state with a store state
// eg.

import {mapState} from 'vuex'

export default {
  data () {
    return {
      value: defaultValue
    }
  },
  computed: {
    ...mapState(['defaultValue'])
  }
}

// this will fail, because 'computed' is evaluated after 'data'
// do this instead

export default {
  data () {
    return {
      value: this.$store.state.defaultValue
    }
  }
}

// this works because '$store' is injected in as a 'prop'
// therefore is available to 'data' during evaluation

Vuex getter with payload

// to retrieve a store state, we either access the property directly
// or call a getter function

const a = store.state.a
const b = store.getters.b

// what if we want a getter that accepts a payload
// eg.

function memberOf (state, target) {
  return state.list.indexOf(target) > -1
}

// with plain Vue instance, it is easy
// simply declare the function under the "methods" property

// however, can we do something with Vuex store?

// we cannot use "getters" since they accept only a single argument: state
// we can use "actions" but that will still not be ideal since actions return promises
// so the (fake) getter will not return synchronously

// the correct way is to create a getter that returns a function
// i.e.

const store = new Vuex.Store({
  state: {
    list: []
  },
  getters: {
    memberOf (state) {
      return (target) {
        return state.list.indexOf(target) > -1
      }
    }
  }
})

// in your component
import {mapGetters} from 'vuex'

export default {
  computed: {
    ...mapGetters(['memberOf'])
  }
}

// then you can call "memberOf" anywhere inside your component as if it is a method

Yong Jun 詠竣

Thank You

谢谢

JSConf Shanghai 2017

Building a modular application with VueJS

By yongjun21

Building a modular application with VueJS

  • 2,383