Last active
November 25, 2019 18:29
-
-
Save Akryum/05964e81d09fb5088b7769cff15f5e7c to your computer and use it in GitHub Desktop.
Example of migration to Vue Function-based Component API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
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
@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:
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 constgql
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.