Skip to content

Instantly share code, notes, and snippets.

@Akryum
Last active November 25, 2019 18:29
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Akryum/05964e81d09fb5088b7769cff15f5e7c to your computer and use it in GitHub Desktop.
Save Akryum/05964e81d09fb5088b7769cff15f5e7c to your computer and use it in GitHub Desktop.
Example of migration to Vue Function-based Component API
<script>
import { isValidMultiName } from '@/util/folders'
import FOLDER_CURRENT from '@/graphql/folder/folderCurrent.gql'
import FOLDERS_FAVORITE from '@/graphql/folder/foldersFavorite.gql'
import FOLDER_OPEN from '@/graphql/folder/folderOpen.gql'
import FOLDER_OPEN_PARENT from '@/graphql/folder/folderOpenParent.gql'
import FOLDER_SET_FAVORITE from '@/graphql/folder/folderSetFavorite.gql'
import PROJECT_CWD_RESET from '@/graphql/project/projectCwdReset.gql'
import FOLDER_CREATE from '@/graphql/folder/folderCreate.gql'
const SHOW_HIDDEN = 'vue-ui.show-hidden-folders'
export default {
data () {
return {
loading: 0,
error: false,
editingPath: false,
editedPath: '',
folderCurrent: {},
foldersFavorite: [],
showHidden: localStorage.getItem(SHOW_HIDDEN) === 'true',
showNewFolder: false,
newFolderName: ''
}
},
apollo: {
folderCurrent: {
query: FOLDER_CURRENT,
fetchPolicy: 'network-only',
loadingKey: 'loading',
async result () {
await this.$nextTick()
this.$refs.folders.scrollTop = 0
}
},
foldersFavorite: FOLDERS_FAVORITE
},
computed: {
newFolderValid () {
return isValidMultiName(this.newFolderName)
}
},
watch: {
showHidden (value) {
if (value) {
localStorage.setItem(SHOW_HIDDEN, 'true')
} else {
localStorage.removeItem(SHOW_HIDDEN)
}
}
},
beforeRouteLeave (to, from, next) {
if (to.matched.some(m => m.meta.needProject)) {
this.resetProjectCwd()
}
next()
},
methods: {
async openFolder (path) {
this.editingPath = false
this.error = null
this.loading++
try {
await this.$apollo.mutate({
mutation: FOLDER_OPEN,
variables: {
path
},
update: (store, { data: { folderOpen } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { folderCurrent: folderOpen } })
}
})
} catch (e) {
this.error = e
}
this.loading--
},
async openParentFolder (folder) {
this.editingPath = false
this.error = null
this.loading++
try {
await this.$apollo.mutate({
mutation: FOLDER_OPEN_PARENT,
update: (store, { data: { folderOpenParent } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { folderCurrent: folderOpenParent } })
}
})
} catch (e) {
this.error = e
}
this.loading--
},
async toggleFavorite () {
await this.$apollo.mutate({
mutation: FOLDER_SET_FAVORITE,
variables: {
path: this.folderCurrent.path,
favorite: !this.folderCurrent.favorite
},
update: (store, { data: { folderSetFavorite } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { folderCurrent: folderSetFavorite } })
let data = store.readQuery({ query: FOLDERS_FAVORITE })
// TODO this is a workaround
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473
data = {
foldersFavorite: data.foldersFavorite.slice()
}
if (folderSetFavorite.favorite) {
data.foldersFavorite.push(folderSetFavorite)
} else {
const index = data.foldersFavorite.findIndex(
f => f.path === folderSetFavorite.path
)
index !== -1 && data.foldersFavorite.splice(index, 1)
}
store.writeQuery({ query: FOLDERS_FAVORITE, data })
}
})
},
cwdChangedUpdate (previousResult, { subscriptionData }) {
return {
cwd: subscriptionData.data.cwd
}
},
async openPathEdit () {
this.editedPath = this.folderCurrent.path
this.editingPath = true
await this.$nextTick()
this.$refs.pathInput.focus()
},
submitPathEdit () {
this.openFolder(this.editedPath)
},
refreshFolder () {
this.openFolder(this.folderCurrent.path)
},
resetProjectCwd () {
this.$apollo.mutate({
mutation: PROJECT_CWD_RESET
})
},
async createFolder () {
if (!this.newFolderValid) return
const result = await this.$apollo.mutate({
mutation: FOLDER_CREATE,
variables: {
name: this.newFolderName
}
})
this.openFolder(result.data.folderCreate.path)
this.newFolderName = ''
this.showNewFolder = false
},
slicePath (path) {
const parts = []
let startIndex = 0
let index
const findSeparator = () => {
index = path.indexOf('/', startIndex)
if (index === -1) index = path.indexOf('\\', startIndex)
return index !== -1
}
const addPart = index => {
const folder = path.substring(startIndex, index)
const slice = path.substring(0, index + 1)
parts.push({
name: folder,
path: slice
})
}
while (findSeparator()) {
addPart(index)
startIndex = index + 1
}
if (startIndex < path.length) addPart(path.length)
return parts
}
}
}
</script>
<script>
import { useQuery, mutate } from 'vue-apollo'
import { nextTick, state, value, watch } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { isValidMultiName } from '@/util/folders'
import FOLDER_CURRENT from '@/graphql/folder/folderCurrent.gql'
import FOLDERS_FAVORITE from '@/graphql/folder/favoriteFolders.gql'
import FOLDER_OPEN from '@/graphql/folder/folderOpen.gql'
import FOLDER_OPEN_PARENT from '@/graphql/folder/folderOpenParent.gql'
import FOLDER_SET_FAVORITE from '@/graphql/folder/folderSetFavorite.gql'
import PROJECT_CWD_RESET from '@/graphql/project/projectCwdReset.gql'
import FOLDER_CREATE from '@/graphql/folder/folderCreate.gql'
const SHOW_HIDDEN = 'vue-ui.show-hidden-folders'
export default {
setup (props, { refs }) {
const { networkState } = useNetworkState()
// Folder
const { currentFolderData } = usecurrentFolderData(networkState)
const folderNavigationFeature = useFolderNavigation({
networkState,
currentFolderData
refs,
})
const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData)
const { showHiddenFolders } = useHiddenFolders()
const createFolderFeature = useCreateFolder(folderNavigationFeature.openFolder)
// Current working directory
const { updateOnCwdChanged } = useCwd()
// Utils
const { slicePath } = usePathUtils()
return {
networkState,
currentFolderData,
...folderNavigationFeature,
refreshFolder,
favoriteFolders,
toggleFavorite,
showHiddenFolders,
...createFolderFeature,
updateOnCwdChanged,
slicePath,
}
}
}
function useNetworkState () {
const networkState = state({
loading: 0,
error: false,
})
return {
networkState
}
}
function usecurrentFolderData (networkState) {
const currentFolderData = useQuery({
query: FOLDER_CURRENT,
fetchPolicy: 'networkState-only',
networkState,
async result () {
await nextTick()
refs.folders.scrollTop = 0
}
}, {})
return {
currentFolderData
}
}
function useFolderNavigation ({ networkState, currentFolderData, refs }) {
// Path editing
const pathEditing = state({
editingPath: false,
editedPath: '',
})
async function openPathEdit () {
pathEditing.editedPath = currentFolderData.path
pathEditing.editingPath = true
await nextTick()
refs.pathInput.focus()
}
function submitPathEdit () {
openFolder(pathEditing.editedPath)
}
// Folder opening
const openFolder = async (path) => {
pathEditing.editingPath = false
networkState.error = null
networkState.loading++
try {
await mutate({
mutation: FOLDER_OPEN,
variables: {
path
},
update: (store, { data: { folderOpen } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderOpen } })
}
})
} catch (e) {
networkState.error = e
}
networkState.loading--
}
async function openParentFolder () {
pathEditing.editingPath = false
networkState.error = null
networkState.loading++
try {
await mutate({
mutation: FOLDER_OPEN_PARENT,
update: (store, { data: { folderOpenParent } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderOpenParent } })
}
})
} catch (e) {
networkState.error = e
}
networkState.loading--
}
// Refresh
function refreshFolder () {
openFolder(currentFolderData.path)
}
return {
pathEditing,
openPathEdit,
submitPathEdit,
openFolder,
openParentFolder,
refreshFolder
}
}
function useFavoriteFolders (currentFolderData) {
const favoriteFolders = useQuery(FOLDERS_FAVORITE, [])
async function toggleFavorite () {
await mutate({
mutation: FOLDER_SET_FAVORITE,
variables: {
path: currentFolderData.path,
favorite: !currentFolderData.favorite
},
update: (store, { data: { folderSetFavorite } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderSetFavorite } })
let data = store.readQuery({ query: FOLDERS_FAVORITE })
// TODO this is a workaround
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473
data = {
favoriteFolders: data.favoriteFolders.slice()
}
if (folderSetFavorite.favorite) {
data.favoriteFolders.push(folderSetFavorite)
} else {
const index = data.favoriteFolders.findIndex(
f => f.path === folderSetFavorite.path
)
index !== -1 && data.favoriteFolders.splice(index, 1)
}
store.writeQuery({ query: FOLDERS_FAVORITE, data })
}
})
}
return {
favoriteFolders,
toggleFavorite
}
}
function useHiddenFolders () {
const showHiddenFolders = value(localStorage.getItem(SHOW_HIDDEN) === 'true')
watch(showHiddenFolders, value => {
if (value) {
localStorage.setItem(SHOW_HIDDEN, 'true')
} else {
localStorage.removeItem(SHOW_HIDDEN)
}
}, { lazy: true })
return {
showHiddenFolders
}
}
function useCwd () {
async function resetProjectCwd () {
await mutate({
mutation: PROJECT_CWD_RESET
})
}
onBeforeRouteLeave((to, from, next) => {
if (to.matched.some(m => m.meta.needProject)) {
resetProjectCwd()
}
next()
})
// Update apollo cache
const updateOnCwdChanged = (previousResult, { subscriptionData }) => {
return {
cwd: subscriptionData.data.cwd
}
}
return {
updateOnCwdChanged
}
}
function useCreateFolder (openFolder) {
const showNewFolder = value(false)
const newFolderName = value('')
const newFolderValid = computed(() => isValidMultiName(newFolderName.value))
async function createFolder () {
if (!newFolderValid.value) return
const result = await mutate({
mutation: FOLDER_CREATE,
variables: {
name: newFolderName.value
}
})
openFolder(result.data.folderCreate.path)
newFolderName.value = ''
showNewFolder.value = false
}
return {
showNewFolder,
newFolderName,
newFolderValid,
createFolder
}
}
function usePathUtils () {
const slicePath = (path) => {
const parts = []
let startIndex = 0
let index
function findSeparator () {
index = path.indexOf('/', startIndex)
if (index === -1) index = path.indexOf('\\', startIndex)
return index !== -1
}
const addPart = index => {
const folder = path.substring(startIndex, index)
const slice = path.substring(0, index + 1)
parts.push({
name: folder,
path: slice
})
}
while (findSeparator()) {
addPart(index)
startIndex = index + 1
}
if (startIndex < path.length) addPart(path.length)
return parts
}
return {
slicePath
}
}
</script>
<script>
import { useQuery, mutate } from 'vue-apollo'
import { nextTick, state, value, watch } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
// Reusable functions not specific to this component
import { useNetworkState } from '@/functions/network'
import { usePathUtils } from '@/functions/path'
import { resetCwdOnLeave, useCwdUtils } from '@/functions/cwd'
// GraphQL
import FOLDER_CURRENT from '@/graphql/folder/folderCurrent.gql'
import FOLDERS_FAVORITE from '@/graphql/folder/favoriteFolders.gql'
import FOLDER_OPEN from '@/graphql/folder/folderOpen.gql'
import FOLDER_OPEN_PARENT from '@/graphql/folder/folderOpenParent.gql'
import FOLDER_SET_FAVORITE from '@/graphql/folder/folderSetFavorite.gql'
import PROJECT_CWD_RESET from '@/graphql/project/projectCwdReset.gql'
import FOLDER_CREATE from '@/graphql/folder/folderCreate.gql'
// Misc
import { isValidMultiName } from '@/util/folders'
const SHOW_HIDDEN = 'vue-ui.show-hidden-folders'
export default {
setup (props, { refs }) {
const { networkState } = useNetworkState()
// Folder
const { currentFolderData } = usecurrentFolderData(networkState)
const folderNavigationFeature = useFolderNavigation({
networkState,
currentFolderData
refs,
})
const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData)
const { showHiddenFolders } = useHiddenFolders()
const createFolderFeature = useCreateFolder(folderNavigation.openFolder)
// Current working directory
resetCwdOnLeave()
const { updateOnCwdChanged } = useCwdUtils()
// Utils
const { slicePath } = usePathUtils()
return {
networkState,
currentFolderData,
...folderNavigationFeature,
refreshFolder,
favoriteFolders,
toggleFavorite,
showHiddenFolders,
...createFolderFeature,
updateOnCwdChanged,
slicePath,
}
}
}
// Reusable functions specific to this component
export function usecurrentFolderData (networkState) {
const currentFolderData = useQuery({
query: FOLDER_CURRENT,
fetchPolicy: 'networkState-only',
networkState,
async result () {
await nextTick()
refs.folders.scrollTop = 0
}
}, {})
return {
currentFolderData
}
export }
export function useFolderNavigation ({ networkState, currentFolderData, refs }) {
// Path editing
const pathEditing = state({
editingPath: false,
editedPath: '',
})
async function openPathEdit () {
pathEditing.editedPath = currentFolderData.path
pathEditing.editingPath = true
await nextTick()
refs.pathInput.focus()
}
function submitPathEdit () {
openFolder(pathEditing.editedPath)
}
// Folder opening
const openFolder = async (path) => {
pathEditing.editingPath = false
networkState.error = null
networkState.loading++
try {
await mutate({
mutation: FOLDER_OPEN,
variables: {
path
},
update: (store, { data: { folderOpen } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderOpen } })
}
})
} catch (e) {
networkState.error = e
}
networkState.loading--
}
async function openParentFolder () {
pathEditing.editingPath = false
networkState.error = null
networkState.loading++
try {
await mutate({
mutation: FOLDER_OPEN_PARENT,
update: (store, { data: { folderOpenParent } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderOpenParent } })
}
})
} catch (e) {
networkState.error = e
}
networkState.loading--
}
// Refresh
function refreshFolder () {
openFolder(currentFolderData.path)
}
return {
pathEditing,
openPathEdit,
submitPathEdit,
openFolder,
openParentFolder,
refreshFolder
}
}
export function useFavoriteFolders (currentFolderData) {
const favoriteFolders = useQuery(FOLDERS_FAVORITE, [])
async function toggleFavorite () {
await mutate({
mutation: FOLDER_SET_FAVORITE,
variables: {
path: currentFolderData.path,
favorite: !currentFolderData.favorite
},
update: (store, { data: { folderSetFavorite } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderSetFavorite } })
let data = store.readQuery({ query: FOLDERS_FAVORITE })
// TODO this is a workaround
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473
data = {
favoriteFolders: data.favoriteFolders.slice()
}
if (folderSetFavorite.favorite) {
data.favoriteFolders.push(folderSetFavorite)
} else {
const index = data.favoriteFolders.findIndex(
f => f.path === folderSetFavorite.path
)
index !== -1 && data.favoriteFolders.splice(index, 1)
}
store.writeQuery({ query: FOLDERS_FAVORITE, data })
}
})
}
return {
favoriteFolders,
toggleFavorite
}
}
export function useHiddenFolders () {
const showHiddenFolders = value(localStorage.getItem(SHOW_HIDDEN) === 'true')
watch(showHiddenFolders, value => {
if (value) {
localStorage.setItem(SHOW_HIDDEN, 'true')
} else {
localStorage.removeItem(SHOW_HIDDEN)
}
}, { lazy: true })
return {
showHiddenFolders
}
}
export function useCreateFolder (openFolder) {
const showNewFolder = value(false)
const newFolderName = value('')
const newFolderValid = computed(() => isValidMultiName(newFolderName.value))
async function createFolder () {
if (!newFolderValid.value) return
const result = await mutate({
mutation: FOLDER_CREATE,
variables: {
name: newFolderName.value
}
})
openFolder(result.data.folderCreate.path)
newFolderName.value = ''
showNewFolder.value = false
}
return {
showNewFolder,
newFolderName,
newFolderValid,
createFolder
}
}
</script>
@martinsotirov
Copy link

(commenting here, so that we don't spam the RFC thread)

My problem with FileExplorerFunctionsFinal.vue is that it is much less transparent and clear than FileExplorer.vue. I cannot at a quick glance see the whole state, all computed, all watchers etc. Even if all the hook calls were placed in the setup function, I would still have to read it carefully every time to know what's what.

Is this the end of the world? Obviously no, but it does erode Vue's immediate approachability and ease of use. Also, it places way too much responsibility on the developer to make good decisions about how to structure their code. In an ideal world, that's not a problem but 90% of real world devs I have to coach at my clients' teams will struggle to keep things in order with the new API.

@Akryum
Copy link
Author

Akryum commented Jun 25, 2019

I cannot at a quick glance see the whole state, all computed, all watchers etc.

It's basically the return object of setup.

I would still have to read it carefully every time to know what's what.

How I see it is that it would actually be easier than before. For example, a new requirement comes to this component that is it should automatically check if the new folder name being typed already exists to display a warning and disable the create button in the corresponding modal (not shown in the example code).

You can easily find where the folder creation data and logic lives in the code and make adjustment, without even bothering looking at the rest of the code.

1- Look at the setup function
2- Ctrl + click on useCreateFolder to go to definition (on VS Code)
3- Make some changes
4- Save!

@martinsotirov
Copy link

You can easily find where the folder creation data and logic lives in the code and make adjustment, without even bothering looking at the rest of the code.

I see what you mean but the problem this is solving is that your FileExplorer.vue is just not well implemented. It's too big and does too many things with business logic mixed in with the UI. You have to break it down and abstract away things like the file creation logic via Vuex or even a vanilla js service class.

@Akryum
Copy link
Author

Akryum commented Jun 25, 2019

True it could be split in multiple components, but then the setup function would be very small and your initial concern wouldn't apply?

@Akryum
Copy link
Author

Akryum commented Jun 25, 2019

business logic mixed in with the UI

I don't agree here, I don't like MVC and I do think the logic should be encapsulated in components as much as possible.

@martinsotirov
Copy link

True it could be split in multiple components, but then the setup function would be very small and your initial concern wouldn't apply?

Yes, but also there would be no need for setup and all the hooks in the first place because each component would be small enough to be manageable.

Do you put all your CSS in a style folder?

Yes, absolutely. Styles have no place in JavaScript.

@Akryum
Copy link
Author

Akryum commented Jun 25, 2019

even a vanilla js service class

Yes, absolutely. Styles have no place in JavaScript.

🙅‍♂️

@Akryum
Copy link
Author

Akryum commented Jun 25, 2019

Yes, but also there would be no need for setup and all the hooks in the first place because each component would be small enough to be manageable.

Having too many components also has its issues: performance impact, unmanageable tree in the devtools, more cognitive load to understand the flows... While the setup API doesn't have those drawbacks.

@martinsotirov
Copy link

martinsotirov commented Jun 25, 2019

How is the cognitive load of a massive component with dozens of hook calls less than that of multiple simple components that each do one thing well?

Proper clean code architecture is not at all about MVC. It's about separation of concerns so that changes are easier to implement. What happens when you need to refactor some API call from GraphQL to a normal REST request? Suddenly you have to update 10 Vue components littered with Apollo calls instead one single service that abstracts your API.

@Akryum
Copy link
Author

Akryum commented Jun 25, 2019

How is the cognitive load of a massive component with dozens of hook calls less than that of multiple simple components that each do one thing well?

There would be even simpler plain functions each doing one thing well with very simple ways to interact with each other (plain variables). So yes cognitive load would be even lower.

separation of concerns

It seems I have a very different point of view on this subject than you and I guess it's fine. My opinion is that you are mostly doing something closer to separation of technologies instead.

What happens when you need to refactor some API call from GraphQL to a normal REST request? Suddenly you have to update 10 Vue components littered with Apollo calls instead one single service that abstracts your API.

One of the major benefits of GraphQL and Apollo is co-locating data requirements in the relevant components themselves.

one single service that abstracts your API.

Please don't do GraphQL like this 🙀

Isn't the conversation going a little bit off-topic here? 😸

@martinsotirov
Copy link

My opinion is that you are mostly doing something closer to separation of technologies instead.

No, I am talking about separation of concerns and single responsibility principle – this means that, for example, changing styles, shouldn't force you to update the files where business logic resides or changing something about the database shouldn't force you to change styles or views and so on. You might want to read up on Design Patterns, the SOLID principle, Domain Driven Development etc. These are basic programming concepts, I'm not talking about anything new here...

One of the major benefits of GraphQL and Apollo is co-locating data requirements in the relevant components themselves.

This absolutely doesn't solve the problem I described. What happens if you decide to refactor to a different library instead of Apollo? You still have to change a bunch of unrelated files. The view shouldn't care how or where the data comes from.

Isn't the conversation going a little bit off-topic here?

Yes, I see that we are talking past each other so I am leaving.

@Akryum
Copy link
Author

Akryum commented Jun 25, 2019

shouldn't force you to update the files where business logic resides or changing something about the database shouldn't force you to change styles or views and so on.

Editing a file shouldn't be a metric of how good you separate concerns. Maybe a component-based architecture isn't the right abstraction for you. Because in a component architecture, components should be as self-sufficient and encapsulated as possible, so that concerned are cleanly separated and maintainability is as high as possible.

What happens if you decide to refactor to a different library instead of Apollo?

So you are saying you would have to edit dozens of JavaScript file instead of dozens of Vue files. There is not much difference. The more I read you, the more I'm convinced you are mixing up separation of concerns and separation of technologies. Maybe I'm wrong, I don't know the projects you are working on.

You still have to change a bunch of unrelated files.

Those files are related, they are components of your application. But at the same time they each (should) have their own responsibilities, at user-experience level.

@martinsotirov
Copy link

martinsotirov commented Jun 25, 2019

Editing a file shouldn't be a metric of how good you separate concerns.

That's literally the definition of Single Responsibility Principle:

As an example, consider a module that compiles and prints a report. Imagine such a module can be changed for two reasons. First, the content of the report could change. Second, the format of the report could change. These two things change for very different causes; one substantive, and one cosmetic. The single responsibility principle says that these two aspects of the problem are really two separate responsibilities, and should therefore be in separate classes or modules.

I see where the problem stems from.

The more I read you, the more I'm convinced you are mixing up separation of concerns and separation of technologies.

Those files are related, they are components of your application. But at the same time they each (should) have their own responsibilities, at user-experience level.

You are mixing up separation of concerns with domain driven development, where code related to the same functionality is grouped together in the same module, not file).

Here's the definition of Separation of Concerns from Wikipedia:

Modularity, and hence separation of concerns, is achieved by encapsulating information inside a section of code that has a well-defined interface. Encapsulation is a means of information hiding. Layered designs in information systems are another embodiment of separation of concerns (e.g., presentation layer, business logic layer, data access layer, persistence layer).

E.g. a button in your UI shouldn't directly know about the database.

That in no way negates the use case for component architecture. Only you are abusing them by stuffing them with logic that should not be there.

This is from the Vue docs:

Vue (pronounced /vjuː/, like view) is a progressive framework for building user interfaces.

@beeplin
Copy link

beeplin commented Jun 25, 2019

@Akryum @martinsotirov sorry to bother. My 2 cents.

As a full-stack developer, I feel development for web UI has something fundamentally different from traditional backend development. The backend world can be perfectly divided in some purely logical and self-containing layers: web API, business logic, shared services, data access layer, database ... And together with division of logical layers comes very common division of labor. Different people in charge of different tasks. So we have OOP, single responsibility principle, DDD ...

But in web frontend it seems too heavy to apply all the traditional software engineering doctrines. Most UI components are something quite special: relatively simple business logic (since heavy business logic always goes to backend), huge view layer (DOM API, vue template, react jsx...), and more importantly, high frequency of behavior changing. I guess more than 90% of frontend code is about:

  • fetching data from backend
  • restructuring fetched data to feed to UI
  • rendering UI
  • handling UI input events

most of which are heavily entangled with the UI framework (vue & quasar? react & material design? ...) and the API framework (rpc? rest? graphql & apollo?) and are changing very quickly and frequently due to the ever swinging/evolving requirements from product team.

So after trying MVC/MVVM pattern for some time, now I kinda agree with @Akryum and choose to use vue SFC and vue-apollo(graphql), making vue component as my basic self-containing logic unit: a component is like a class instance from the OOP view, having its own states, behavior (including data fetching, css styles, UI rendering, etc.). All in one component. (EDIT: unlike what @Akryum does above, I even tend not to import gql files into components, but rather to write const gql strings directly in the method bodies of components, in order to make it clear what kind of queries/mutations I am doing here.)

Yes, this leads to heavy framework binding. it is hard to transfer from vue to react, or from apollo to rest. But, anyway there are no that much business logics in the frontend, therefore even if we had carefully designed a framework-independent architecture and had extracted all pure business logics into service files ... we would found that when we really tried, for example, to switch vue to react, there weren't that much codes that can be reused. We basically always had to rewrite the frontend part from scratch ;P

From that aspect I think the new composition function pattern is quite enough, and in fact quite suitable, for what I know as frontend development.

@Akryum
Copy link
Author

Akryum commented Jun 25, 2019

EDIT: unlike what @Akryum does above, I even tend not to import gql files into components, but rather to write const gql strings directly in the method bodies of components, in order to make it clear what kind of queries/mutations I am doing here.

I do that now too, the example above is pending some refactoring in vue-cli-ui. 😸

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment