feat(vue-app): build indicator (#5820)

* feat: inline HMR progress indicator

* support router base

* fix nuxt err

* fix space

* fix indentation

* return in case of ws message parsing error

* close ws on beforeDestroy

* ui: Update loading indicator UI

* builder: Add build.indicator option

* ui: Use only logo and %

* hotfix: Alphabetical order

* hotfix: Add fixed with and add back v-if

* minor style change

* rename component to build-indicator

* feat: animated progress

* assign name to component

* update test

* naming consistency

* render into app to prevent dom wrapping

* extra new line

* better App.js formatting

* update snapshot

* clear interval
This commit is contained in:
Pooya Parsa 2019-05-29 19:14:15 +04:30 committed by Sébastien Chopin
parent e161d70a4f
commit a759196865
9 changed files with 165 additions and 4 deletions

View File

@ -20,6 +20,7 @@ export default class TemplateContext {
isDev: options.dev, isDev: options.dev,
isTest: options.test, isTest: options.test,
debug: options.debug, debug: options.debug,
buildIndicator: options.dev && options.build.indicator,
vue: { config: options.vue.config }, vue: { config: options.vue.config },
fetch: options.fetch, fetch: options.fetch,
mode: options.mode, mode: options.mode,

View File

@ -7,6 +7,7 @@ TemplateContext {
], ],
"templateVars": Object { "templateVars": Object {
"appPath": "./App.js", "appPath": "./App.js",
"buildIndicator": undefined,
"components": Object { "components": Object {
"ErrorPage": "relativeBuild(test_error_page)", "ErrorPage": "relativeBuild(test_error_page)",
}, },

View File

@ -3,6 +3,7 @@ import env from 'std-env'
export default () => ({ export default () => ({
quiet: Boolean(env.ci || env.test), quiet: Boolean(env.ci || env.test),
analyze: false, analyze: false,
indicator: true,
profile: process.argv.includes('--profile'), profile: process.argv.includes('--profile'),
extractCSS: false, extractCSS: false,
crossorigin: undefined, crossorigin: undefined,

View File

@ -54,6 +54,7 @@ Object {
"useShortDoctype": true, "useShortDoctype": true,
}, },
}, },
"indicator": true,
"loaders": Object { "loaders": Object {
"css": Object { "css": Object {
"sourceMap": false, "sourceMap": false,

View File

@ -41,6 +41,7 @@ Object {
"useShortDoctype": true, "useShortDoctype": true,
}, },
}, },
"indicator": true,
"loaders": Object { "loaders": Object {
"css": Object {}, "css": Object {},
"cssModules": Object { "cssModules": Object {
@ -374,6 +375,7 @@ Object {
"useShortDoctype": true, "useShortDoctype": true,
}, },
}, },
"indicator": true,
"loaders": Object { "loaders": Object {
"css": Object {}, "css": Object {},
"cssModules": Object { "cssModules": Object {

View File

@ -49,6 +49,7 @@ export interface NuxtConfigurationBuild {
hardSource?: boolean hardSource?: boolean
hotMiddleware?: WebpackHotMiddlewareOptions hotMiddleware?: WebpackHotMiddlewareOptions
html?: { minify: HtmlMinifierOptions } html?: { minify: HtmlMinifierOptions }
indicator?: boolean
loaders?: NuxtConfigurationLoaders loaders?: NuxtConfigurationLoaders
optimization?: WebpackOptions.Optimization optimization?: WebpackOptions.Optimization
optimizeCSS?: OptimizeCssAssetsWebpackPluginOptions | boolean optimizeCSS?: OptimizeCssAssetsWebpackPluginOptions | boolean

View File

@ -13,6 +13,7 @@ export const template = {
'server.js', 'server.js',
'utils.js', 'utils.js',
'empty.js', 'empty.js',
'components/nuxt-build-indicator.vue',
'components/nuxt-error.vue', 'components/nuxt-error.vue',
'components/nuxt-loading.vue', 'components/nuxt-loading.vue',
'components/nuxt-child.js', 'components/nuxt-child.js',

View File

@ -1,5 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
<% if (loading) { %>import NuxtLoading from '<%= (typeof loading === "string" ? loading : "./components/nuxt-loading.vue") %>'<% } %> <% if (loading) { %>import NuxtLoading from '<%= (typeof loading === "string" ? loading : "./components/nuxt-loading.vue") %>'<% } %>
<%if (buildIndicator) { %>import NuxtBuildIndicator from './components/nuxt-build-indicator'<% } %>
<% css.forEach((c) => { %> <% css.forEach((c) => { %>
import '<%= relativeToBuild(resolvePath(c.src || c, { isStyle: true })) %>' import '<%= relativeToBuild(resolvePath(c.src || c, { isStyle: true })) %>'
<% }) %> <% }) %>
@ -49,10 +50,7 @@ export default {
domProps: { domProps: {
id: '<%= globals.id %>' id: '<%= globals.id %>'
} }
}, [ }, [<% if (loading) { %>loadingEl, <% } %><%if (buildIndicator) { %>h(NuxtBuildIndicator), <% } %>transitionEl])
<% if (loading) { %>loadingEl,<% } %>
transitionEl
])
}, },
data: () => ({ data: () => ({
isOnline: true, isOnline: true,

View File

@ -0,0 +1,155 @@
<template>
<transition appear>
<div class="nuxt__build_indicator" v-if="building">
<svg viewBox="0 0 96 72" version="1" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M6 66h23l1-3 21-37L40 6 6 66zM79 66h11L62 17l-5 9 22 37v3zM54 31L35 66h38z"/>
<path d="M29 69v-1-2H6L40 6l11 20 3-6L44 3s-2-3-4-3-3 1-5 3L1 63c0 1-2 3 0 6 0 1 2 2 5 2h28c-3 0-4-1-5-2z" fill="#00C58E"/>
<path d="M95 63L67 14c0-1-2-3-5-3-1 0-3 0-4 3l-4 6 3 6 5-9 28 49H79a5 5 0 0 1 0 3c-2 2-5 2-5 2h16c1 0 4 0 5-2 1-1 2-3 0-6z" fill="#00C58E"/>
<path d="M79 69v-1-2-3L57 26l-3-6-3 6-21 37-1 3a5 5 0 0 0 0 3c1 1 2 2 5 2h40s3 0 5-2zM54 31l19 35H35l19-35z" fill="#FFF" fill-rule="nonzero"/>
</g>
</svg>
{{ animatedProgress }}%
</div>
</transition>
</template>
<script>
export default {
name: 'nuxt-build-indicator',
data() {
return {
building: false,
progress: 0,
animatedProgress: 0,
reconnectAttempts: 0,
}
},
mounted() {
if (WebSocket === undefined) {
return // Unsupported
}
this.wsConnect('<%= router.base %>_loading/ws')
},
beforeDestroy() {
this.wsClose()
},
watch: {
progress(val, oldVal) {
// Cancel old animation
clearInterval(this._progressAnimation)
// Average progress may decrease but ignore it!
if (val < oldVal) {
return
}
// Jump to edge imediately
if (val < 10 || val > 90) {
this.animatedProgress = val
}
// Animate to value
this._progressAnimation = setInterval(() => {
const diff = this.progress - this.animatedProgress
if (diff > 0) {
this.animatedProgress++
} else {
clearInterval(this._progressAnimation)
}
}, 50)
}
},
methods: {
wsConnect(path) {
if (path) {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
this.wsURL = `${protocol}://${location.hostname}:${location.port}${path}`
}
this.ws = new WebSocket(this.wsURL)
this.ws.onclose = this.onWSClose.bind(this)
this.ws.onerror = this.onWSError.bind(this)
this.ws.onmessage = this.onWSMessage.bind(this)
},
wsReconnect(e) {
this.reconnectAttempts++
if (this.reconnectAttempts > 10) {
return
}
setTimeout(() => { this.wsConnect() }, 1000)
},
onWSClose(e) {
// https://tools.ietf.org/html/rfc6455#section-11.7
if (e.code !== 1000 && e.code !== 1005) {
this.wsReconnect() // Unkown error
}
},
onWSError(error) {
if (error.code === 'ECONNREFUSED') {
this.wsReconnect(error)
}
},
onWSMessage(msg) {
let data = msg.data
try {
if (data[0] === '{') {
data = JSON.parse(data)
}
} catch (e) {
return
}
this.progress = Math.round(data.states.reduce((p, s) => p + s.progress, 0) / data.states.length)
if (!data.allDone) {
this.building = true
} else {
this.$nextTick(() => {
this.building = false
this.animatedProgress = 0
this.progress = 0
clearInterval(this._progressAnimation)
})
}
},
wsClose() {
if (this.ws) {
this.ws.close()
delete this.ws
}
}
}
}
</script>
<style scopped>
.nuxt__build_indicator {
position: absolute;
font-family: monospace;
bottom: 20px;
right: 20px;
background-color: #2E495E;
padding: 5px 10px;
border-radius: 2px;
box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.2);
color: #00C48D;
width: 54px;
}
.v-enter-active, .v-leave-active {
transition-delay: 0.2s;
transition-property: all;
transition-duration: 0.3s;
}
.v-leave-to {
opacity: 0;
transform: translateY(20px);
}
svg {
width: 1.1em;
position: relative;
top: 1px;
}
</style>