1
1
mirror of https://github.com/aelve/guide.git synced 2024-11-22 03:12:58 +03:00

Client preload rewritten (#417)

This commit is contained in:
avele 2019-11-26 17:21:20 +04:00 committed by GitHub
parent 862aed5908
commit 571b67bc84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 157 additions and 137 deletions

View File

@ -2,7 +2,9 @@
<v-app>
<toolbar />
<main class="app-content content-centered">
<router-view />
<ClientServerPrefetch>
<RouterView />
</ClientServerPrefetch>
</main>
<a-footer />
</v-app>
@ -14,24 +16,21 @@ import Component from 'vue-class-component'
import { Watch } from 'vue-property-decorator'
import AFooter from 'client/components/AFooter.vue'
import Toolbar from 'client/components/Toolbar.vue'
import ClientServerPrefetch from 'client/components/ClientServerPrefetch.vue'
import * as nprogress from 'nprogress'
import 'nprogress/nprogress.css'
import { getRouteDocumentTitle } from 'client/router'
nprogress.configure({ showSpinner: false })
@Component({
components: {
Toolbar,
AFooter
AFooter,
ClientServerPrefetch
}
})
export default class RootComponent extends Vue {
beforeMount () {
// This package can only be loaded after mounted (on client only) cause it uses "document"
// it is used in MarkdownEditor.vue and to make it work faster in that component we preload it here
import('easymde')
}
get isPageLoading () {
return this.$store.state.isPageLoading
}
@ -39,6 +38,16 @@ export default class RootComponent extends Vue {
@Watch('isPageLoading')
toogleLoading (isPageLoading: boolean) {
isPageLoading ? nprogress.start() : nprogress.done()
if (!isPageLoading) {
document.title = getRouteDocumentTitle(this.$route)
}
}
beforeMount () {
// This package can only be loaded after mounted (on client only) cause it uses "document"
// it is used in MarkdownEditor.vue and to make it work faster in that component we preload it here
import('easymde')
}
}
</script>

View File

@ -0,0 +1,76 @@
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { Watch } from 'vue-property-decorator'
function getComponentAndItsChildren (component, result?) {
if (!result) {
result = []
}
if (!result.includes(component)) {
result.push(component)
}
const children = component.$children
children.forEach(x => getComponentAndItsChildren(x, result))
return result
}
/**
* When this component slot (supports only one slot) changes it prerenders new slot in memory
* and waits for slot (and its' children) "serverPrefetch" execution before rendering it
*/
@Component
export default class ClientServerPrefetch extends Vue {
// vNodeToRender is not set here (as reactive property) to prevent automatic "render" function call when it changes
isServerPrefetchProcessing = false
@Watch('isServerPrefetchProcessing')
onServerPrefetch (val) {
this.$store.commit('togglePageLoading')
}
onServerPrefetchFinish () {
const previousVNode = this.vNodeToRender
this.vNodeToRender = this.$slots.default[0]
this.$forceUpdate()
this.isServerPrefetchProcessing = false
previousVNode.data.keepAlive = false
previousVNode.componentInstance.$destroy()
}
async preloadSlotComponent () {
this.isServerPrefetchProcessing = true
try {
const slot = this.$slots.default[0]
// This is vue's internal function that inits component
this._update(slot, false)
const componentInstance = slot.componentInstance
const components = getComponentAndItsChildren(componentInstance)
const getServerPrefetchFunc = x => x.$options.serverPrefetch && x.$options.serverPrefetch[0]
const componentsWithServerPrefetch = components.filter(getServerPrefetchFunc)
await Promise.all(componentsWithServerPrefetch.map(x => getServerPrefetchFunc(x).call(x)))
this.onServerPrefetchFinish()
} catch (e) {
this.isServerPrefetchProcessing = false
this.$router.back()
}
}
render (h) {
const slot = this.$slots.default[0]
// If we don't set keepAlive to true, component will be automatically destroyed when slot changes
// and also new slot will be rendered twice: 1 - in preloadSlotComponent func, 2 - when return here
slot.data.keepAlive = true
if (!this.vNodeToRender || slot === this.vNodeToRender || slot.tag === this.vNodeToRender.tag) {
this.vNodeToRender = slot
} else {
// if slot was changed we dont render it immediatly but firstly execute its (and its children) 'serverPrefetch' functions
this.preloadSlotComponent()
}
return this.vNodeToRender
}
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<v-form
class="search-bar"
@keydown.enter.native.prevent="processSearchInput"
@keydown.enter.native.prevent="processSearchQuery"
>
<v-text-field
solo
@ -10,7 +10,7 @@
label="Search in all pages"
class="search-bar__input"
ref="input"
v-model="searchInput"
v-model="searchQuery"
@focus="isFocused = true"
@blur="isFocused= false"
>
@ -25,7 +25,7 @@
size="0.9rem"
class="search-bar__input__clear-btn"
:color="inputIconsColor"
v-show="searchInput"
v-show="searchQuery"
@click="clear"
>$vuetify.icons.times</v-icon>
</v-text-field>
@ -40,19 +40,17 @@ import Component from 'vue-class-component'
export default class SearchBar extends Vue {
isFocused = false
searchQuery = this.$store.state.wiki.searchQuery
processSearchInput () {
this.$router.push({ name: 'SearchResults', query: { query: this.searchInput } })
}
// TODO investigate is using computed necessary here
// TODO serever rendering doesnt render value in text field
get searchInput () {
return this.$store.state.wiki.searchInput
}
set searchInput (value) {
this.$store.commit('wiki/setSearchInput', value)
processSearchQuery () {
const searchResultsRouteName = 'SearchResults'
const isSearchCurrentRoute = this.$route.name === searchResultsRouteName
// same commit statement called in router 'SearchResults' route's "beforeEnter", but if its same route its not called
// searchQuery is watched used in "SearchResults" component
if (isSearchCurrentRoute) {
this.$store.commit('wiki/setSearchQuery', this.searchQuery)
}
this.$router.push({ name: searchResultsRouteName, query: { query: this.searchQuery } })
}
get inputIconsColor () {
@ -60,7 +58,7 @@ export default class SearchBar extends Vue {
}
clear () {
this.searchInput = ''
this.searchQuery = ''
}
focus () {

View File

@ -1,5 +1,3 @@
import _get from 'lodash/get'
import { createApp } from './app'
const { app, router, store } = createApp()
@ -20,70 +18,5 @@ if (store.state.is404) {
}
router.onReady(() => {
registerRouterHooks()
app.$mount('#app')
})
function registerRouterHooks () {
router.beforeEach(async (to, from, next) => {
// This case handles navigation to anchors on same page
if (to.path === from.path) {
next()
return
}
store.commit('tooglePageLoading')
if (!to.matched.length) {
store.commit('tooglePageLoading')
next()
return
}
try {
const propsOption = to.matched[0].props.default
const props = propsOption
? typeof propsOption === 'function'
? propsOption(to)
: typeof propsOption === 'object'
? propsOption
: to.params
: {}
const routeComponent = to.matched[0].components.default
const matchedRootComponent = routeComponent.cid // Check if component already imported
? routeComponent
: (await routeComponent()).default
const matchedComponentsAndChildren = getComponentAndItsChildren(matchedRootComponent)
await Promise.all(matchedComponentsAndChildren.map(component => {
const serverPrefetch = component.options.serverPrefetch && component.options.serverPrefetch[0]
if (typeof serverPrefetch === 'function') {
return serverPrefetch.call({
$store: store,
$router: router,
...component.options.methods,
...props
})
}
}))
next()
} finally {
store.commit('tooglePageLoading')
}
})
}
function getComponentAndItsChildren (component, result?) {
if (!result) {
result = []
}
if (!component.options) {
return result
}
if (!result.includes(component)) {
result.push(component)
}
const children = Object.values(component.options.components)
// Parent component is also presents in components object
.filter(x => x !== component)
children.forEach(x => getComponentAndItsChildren(x, result))
return result
}

View File

@ -1,5 +1,6 @@
import _get from 'lodash/get'
import { createApp } from './app'
import { getRouteDocumentTitle } from './router'
export default async context => {
return new Promise((resolve, reject) => {
@ -19,6 +20,7 @@ export default async context => {
context.rendered = () => {
context.state = store.state
}
context.title = getRouteDocumentTitle(router.currentRoute)
resolve(app)
}, reject)
})

View File

@ -28,18 +28,8 @@ export default class CategoryPage extends Vue {
return this.$store.state.category.category
}
beforeMount () {
this.$watch('category', this.setDocumentTitle, { immediate: true })
}
beforeDestroy () {
this.$store.commit('category/setCategory', null)
}
setDocumentTitle (category) {
document.title = category
? `${category.title} Aelve Guide`
: 'Aelve Guide'
}
}
</script>

View File

@ -12,9 +12,5 @@ import Component from 'vue-class-component'
Categories
}
})
export default class Index extends Vue {
beforeMount () {
document.title = `Aelve Guide`
}
}
export default class Index extends Vue {}
</script>

View File

@ -20,10 +20,6 @@ export default class Page404 extends Vue {
beforeDestroy () {
this.$store.commit('set404', false)
}
beforeMount () {
document.title = `Error 404 Aelve Guide`
}
}
</script>

View File

@ -76,7 +76,7 @@
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { Prop, Watch } from 'vue-property-decorator'
import { Watch } from 'vue-property-decorator'
const resultTagCodes = {
category: 'Category',
@ -85,7 +85,9 @@ const resultTagCodes = {
@Component
export default class SearchResults extends Vue {
@Prop(String) query!: string
get query () {
return this.$store.state.wiki.searchQuery
}
get results () {
return this.$store.state.wiki.searchResults
@ -99,26 +101,13 @@ export default class SearchResults extends Vue {
return this.results.filter(x => x.tag === resultTagCodes.item)
}
beforeMount () {
// This watch should be added in beforeMount (only on client) hook because setDocumentTitle function uses DOM api which is undefined on server
this.$watch('query', this.setDocumentTitle, { immediate: true })
}
mounted () {
this.$store.commit('wiki/setSearchInput', this.query)
}
async serverPrefetch () {
await this.search()
}
@Watch('query')
async search () {
await this.$store.dispatch('wiki/search', this.query)
}
setDocumentTitle (query) {
document.title = `${query} Search results Aelve Guide`
async serverPrefetch () {
await this.search()
}
}
</script>

View File

@ -1,5 +1,6 @@
import Router from 'vue-router'
import categoryPathToId from 'client/helpers/categoryPathToId'
import _get from 'lodash/get'
function createRouter (store) {
return new Router({
@ -32,23 +33,53 @@ function createRouter (store) {
path: '/haskell/:category',
name: 'Category',
component: () => import('../page/CategoryPage.vue'),
props: (route) => ({ categoryId: categoryPathToId(route.params.category) })
props: (route) => ({ categoryId: categoryPathToId(route.params.category) }),
meta: {
documentTitle: () => {
const category = store.state.category.category
return category
? `${category.title} Aelve Guide`
: 'Aelve Guide'
}
}
},
{
path: '/haskell/search/results/',
name: 'SearchResults',
component: () => import('../page/SearchResults.vue'),
props: (route) => ({ query: route.query.query })
beforeEnter: (to, from, next) => {
store.commit('wiki/setSearchQuery', to.query.query)
next()
},
meta: {
documentTitle: (route) => `${route.query.query} Search results Aelve Guide`,
}
},
{
path: '*',
name: 'Page404',
component: () => import('../page/Page404.vue')
component: () => import('../page/Page404.vue'),
meta: {
documentTitle: 'Error 404 Aelve Guide'
}
}
]
})
}
export {
createRouter
function getRouteDocumentTitle (route) {
const defaultTitle = 'Aelve Guide'
const routeDocumentTitle = _get(route, 'meta.documentTitle')
if (routeDocumentTitle) {
return typeof routeDocumentTitle === 'function'
? routeDocumentTitle(route)
: routeDocumentTitle
} else {
return defaultTitle
}
}
export {
createRouter,
getRouteDocumentTitle
}

View File

@ -11,7 +11,7 @@ function createStore () {
},
actions: {},
mutations: {
tooglePageLoading (state) {
togglePageLoading (state) {
state.isPageLoading = !state.isPageLoading
},
set404 (state, val) {

View File

@ -4,12 +4,12 @@ import { set } from '../helpers'
interface IWikiState {
searchResults: any[],
searchInput: string
searchQuery: string
}
const state = (): IWikiState => ({
searchResults: [],
searchInput: ''
searchQuery: ''
})
const getters: GetterTree<IWikiState, any> = {}
@ -23,7 +23,7 @@ const actions: ActionTree<IWikiState, any> = {
const mutations: MutationTree<IWikiState> = {
setSearchResults: set('searchResults'),
setSearchInput: set('searchInput')
setSearchQuery: set('searchQuery')
}
const wiki: Module<IWikiState, any> = {

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Aelve Guide </title>
<title> {{ title }} </title>
</head>
<body>