Better Vue File Organization

Update 2017-08-10: after post this article on /r/vue, one person commented that we don’t need to pass the context since the function will refer to the context where it is called so now in the end of the article we’ll have an even cleaner way to organize our files!


Before we see an alternative way to organize our vue files/components, I must say although I believe it’s a better approach, in fact, there aren’t right or wrong way, we should follow the way we feel more comfortable to work with.

The following piece of code was extracted from the rss-reader project.

First, we’ll see the original file, and after, the proposed way to organize it.

The mental process

  1. Put all functions in the bottom of the file.
  2. When a function needs to access this (the current context), we should pass it as the last parameter to our functions.
  3. The ctx parameter you will see is an abbreviation to context.

Reasons to change

  1. Better way to visualize all methods we have in the file.
  2. Easier way to extract a function to another file if/when necessary.
  3. More “functional” style (!?).

Let’s see and compare the traditional approach with the suggested/alternative one.

Traditional approach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<script>
import Feed from '../../helpers/feeds'
import Favicon from '../../helpers/favicon'
import queue from '../../helpers/queue'
import service from '../../helpers/services'
import { addArticles, addFeed } from '../../vuex/actions'
import _ from 'lodash'
import async from 'async'
export default {
vuex: {
getters: {
offline: state => state.offline,
feeds: state => state.feeds
},
actions: {
addArticles,
addFeed
}
},
computed: {
feedData () {
return this.feeds.map(item => {
if (item.title.length >= 20) {
item.origtitle = item.title
}
item.title = _.truncate(item.title, { length: 20 })
return item
})
}
},
data () {
return {
feedurl: '',
alertmessage: '',
showModal: false,
processed: false
}
},
methods: {
allArticles () {
return this.$route.router.go({path: '/', replace: true})
},
tags () {
return this.$route.router.go({path: '/tags', replace: true})
},
favourites () {
return this.$route.router.go({path: '/article/favourites'})
},
goFeed (title) {
return this.$route.router.go({path: '/feed/' + title})
},
readArticles () {
return this.$route.router.go({path: '/article/read'})
},
unreadArticles () {
return this.$route.router.go({path: '/article/unread'})
},
fetchFeed (callback) {
let feed = new Feed(this.feedurl)
feed.init().then(result => {
if (result === null) {
let error = 'Sorry. I couldn\'t figure out any RSS feed on this address. Try to find link to RSS feed on that site by yourself and paste it here.'
callback(error)
} else {
callback(null, result)
}
}, err => {
if (err) {}
let error = 'Sorry. Unfortunately this website is not supported.'
callback(error)
})
},
checkFeed (data, callback) {
service.checkFeed(data.meta.title, count => {
if (count === 0) {
callback(null, data)
} else {
callback('Feed exists')
}
})
},
fetchIcon (data, callback) {
let favicon = new Favicon(data.meta.link)
favicon.init().then(result => {
let path
if (result !== null) {
path = queue.queueTask('favicon', result)
} else {
path = null
}
data.meta.favicon = path
data.meta.count = data.articles.length
callback(null, data)
})
},
addFeedItem (data, callback) {
this.addFeed(data.meta, result => {
data.meta = result
callback(null, data)
})
},
addArticleItems (data, callback) {
data.articles.map(item => {
let htmlFilename = queue.queueTask('html', item.link)
item.feed = data.meta.title
item.feed_id = data.meta._id
item.file = htmlFilename
item.favicon = data.meta.favicon
return item
})
this.addArticles(data.articles)
callback(null, 'done')
},
addFeedData () {
let self = this
this.processed = true
async.waterfall([
this.fetchFeed,
this.checkFeed,
this.fetchIcon,
this.addFeedItem,
this.addArticleItems
], (err, result) => {
if (!err) {
self.processed = false
self.showModal = false
this.feedurl = ''
} else {
self.alert = true
self.alertmessage = err
self.processed = false
}
})
}
}
}
</script>

Alternative approach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<script>
import Feed from '../../helpers/feeds'
import Favicon from '../../helpers/favicon'
import queue from '../../helpers/queue'
import service from '../../helpers/services'
import { addArticles, addFeed } from '../../vuex/actions'
import _ from 'lodash'
import async from 'async'
export default {
vuex: {
getters: {
offline: state => state.offline,
feeds: state => state.feeds
},
actions: {
addArticles,
addFeed
}
},
computed: {
feedData() { feedData(this) }
},
data () {
return {
feedurl: '',
alertmessage: '',
showModal: false,
processed: false
}
},
methods: {
allArticles() { allArticles(this) },
tags() { tags(this) },
favourites() { favourites(this) },
goFeed() { goFeed(title, this) },
readArticles() { readArticles(this) },
unreadArticles() { unreadArticles(this) },
fetchFeed() { fetchFeed(callback, this) },
checkFeed() { checkFeed(data, callback) },
fetchIcon() { fetchIcon (data, callback) },
addFeedItem() { addFeedItem(data, callback, this) },
addArticleItems() { addArticleItems(data, callback, this) },
addFeedData() { addFeedData(this) }
}
}
////////// Computed Properties
function feedData(ctx) {
return ctx.feeds.map(item => {
if (item.title.length >= 20) {
item.origtitle = item.title
}
item.title = _.truncate(item.title, { length: 20 })
return item
})
}
////////// Methods
function allArticles(ctx) {
return ctx.$route.router.go({path: '/', replace: true})
}
function tags(ctx) {
return ctx.$route.router.go({path: '/tags', replace: true})
}
function favourites(ctx) {
return ctx.$route.router.go({path: '/article/favourites'})
}
function goFeed(title, ctx) {
return ctx.$route.router.go({path: '/feed/' + title})
}
function readArticles(ctx) {
return ctx.$route.router.go({path: '/article/read'})
}
function unreadArticles(ctx) {
return ctx.$route.router.go({path: '/article/unread'})
}
function fetchFeed(callback, ctx) {
let feed = new Feed(ctx.feedurl)
feed.init().then(result => {
if (result === null) {
let error = 'Sorry. I couldn\'t figure out any RSS feed on this address. Try to find link to RSS feed on that site by yourself and paste it here.'
callback(error)
} else {
callback(null, result)
}
}, err => {
if (err) {}
let error = 'Sorry. Unfortunately this website is not supported.'
callback(error)
})
}
function checkFeed(data, callback) {
service.checkFeed(data.meta.title, count => {
if (count === 0) {
callback(null, data)
} else {
callback('Feed exists')
}
})
}
function fetchIcon(data, callback) {
let favicon = new Favicon(data.meta.link)
favicon.init().then(result => {
let path
if (result !== null) {
path = queue.queueTask('favicon', result)
} else {
path = null
}
data.meta.favicon = path
data.meta.count = data.articles.length
callback(null, data)
})
}
function addFeedItem(data, callback, ctx) {
ctx.addFeed(data.meta, result => {
data.meta = result
callback(null, data)
})
}
function addArticleItems(data, callback, ctx) {
data.articles.map(item => {
let htmlFilename = queue.queueTask('html', item.link)
item.feed = data.meta.title
item.feed_id = data.meta._id
item.file = htmlFilename
item.favicon = data.meta.favicon
return item
})
ctx.addArticles(data.articles)
callback(null, 'done')
}
function addFeedData(ctx) {
let self = ctx
ctx.processed = true
async.waterfall([
ctx.fetchFeed,
ctx.checkFeed,
ctx.fetchIcon,
ctx.addFeedItem,
ctx.addArticleItems
], (err, result) => {
if (!err) {
self.processed = false
self.showModal = false
ctx.feedurl = ''
} else {
self.alert = true
self.alertmessage = err
self.processed = false
}
})
}
</script>

Better and cleaner approach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<script>
import Feed from '../../helpers/feeds'
import Favicon from '../../helpers/favicon'
import queue from '../../helpers/queue'
import service from '../../helpers/services'
import { addArticles, addFeed } from '../../vuex/actions'
import _ from 'lodash'
import async from 'async'
export default {
vuex: {
getters: {
offline: state => state.offline,
feeds: state => state.feeds
},
actions: {
addArticles,
addFeed
}
},
computed: {
feedData
},
data () {
return {
feedurl: '',
alertmessage: '',
showModal: false,
processed: false
}
},
methods: {
allArticles,
tags,
favourites,
goFeed,
readArticles,
unreadArticles,
fetchFeed,
checkFeed,
fetchIcon,
addFeedItem,
addArticleItems,
addFeedData
}
}
////////// Computed Properties
function feedData() {
return this.feeds.map(item => {
if (item.title.length >= 20) {
item.origtitle = item.title
}
item.title = _.truncate(item.title, { length: 20 })
return item
})
}
////////// Methods
function allArticles() {
return this.$route.router.go({path: '/', replace: true})
}
function tags() {
return this.$route.router.go({path: '/tags', replace: true})
}
function favourites() {
return this.$route.router.go({path: '/article/favourites'})
}
function goFeed(title) {
return this.$route.router.go({path: '/feed/' + title})
}
function readArticles() {
return this.$route.router.go({path: '/article/read'})
}
function unreadArticles() {
return this.$route.router.go({path: '/article/unread'})
}
function fetchFeed(callback) {
let feed = new Feed(this.feedurl)
feed.init().then(result => {
if (result === null) {
let error = 'Sorry. I couldn\'t figure out any RSS feed on this address. Try to find link to RSS feed on that site by yourself and paste it here.'
callback(error)
} else {
callback(null, result)
}
}, err => {
if (err) {}
let error = 'Sorry. Unfortunately this website is not supported.'
callback(error)
})
}
function checkFeed(data, callback) {
service.checkFeed(data.meta.title, count => {
if (count === 0) {
callback(null, data)
} else {
callback('Feed exists')
}
})
}
function fetchIcon(data, callback) {
let favicon = new Favicon(data.meta.link)
favicon.init().then(result => {
let path
if (result !== null) {
path = queue.queueTask('favicon', result)
} else {
path = null
}
data.meta.favicon = path
data.meta.count = data.articles.length
callback(null, data)
})
}
function addFeedItem(data, callback) {
this.addFeed(data.meta, result => {
data.meta = result
callback(null, data)
})
}
function addArticleItems(data, callback) {
data.articles.map(item => {
let htmlFilename = queue.queueTask('html', item.link)
item.feed = data.meta.title
item.feed_id = data.meta._id
item.file = htmlFilename
item.favicon = data.meta.favicon
return item
})
this.addArticles(data.articles)
callback(null, 'done')
}
function addFeedData() {
let self = this
this.processed = true
async.waterfall([
this.fetchFeed,
this.checkFeed,
this.fetchIcon,
this.addFeedItem,
this.addArticleItems
], (err, result) => {
if (!err) {
self.processed = false
self.showModal = false
this.feedurl = ''
} else {
self.alert = true
self.alertmessage = err
self.processed = false
}
})
}
</script>

Which one do you prefer? Why? Share your thoughts with us :)