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
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,407