feat: ssr with service worker

This commit is contained in:
Pooya Parsa 2020-11-03 23:14:32 +01:00
parent e6fa415e5a
commit 2dbaae6b7d
7 changed files with 129 additions and 12 deletions

View File

@ -3,12 +3,12 @@ import { rollup, OutputOptions } from 'rollup'
import consola from 'consola' import consola from 'consola'
import Hookable from 'hookable' import Hookable from 'hookable'
import defu from 'defu' import defu from 'defu'
import { readFile, writeFile, existsSync } from 'fs-extra' import { existsSync, copy, emptyDir } from 'fs-extra'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import gzipSize from 'gzip-size' import gzipSize from 'gzip-size'
import chalk from 'chalk' import chalk from 'chalk'
import { getRollupConfig } from './rollup.config' import { getRollupConfig } from './rollup.config'
import { tryImport, hl, prettyPath } from './utils' import { tryImport, hl, prettyPath, compileTemplateToJS, renderTemplate } from './utils'
async function main () { async function main () {
const rootDir = resolve(process.cwd(), process.argv[2] || '.') const rootDir = resolve(process.cwd(), process.argv[2] || '.')
@ -18,15 +18,23 @@ async function main () {
rootDir, rootDir,
buildDir: '', buildDir: '',
targets: [], targets: [],
templates: [],
nuxt: 2, nuxt: 2,
target: process.argv[3] && process.argv[3][0] !== '-' ? process.argv[3] : null, target: process.argv[3] && process.argv[3][0] !== '-' ? process.argv[3] : null,
minify: process.argv.includes('--minify') ? true : null, minify: process.argv.includes('--minify') ? true : null,
analyze: process.argv.includes('--analyze') ? true : null, analyze: process.argv.includes('--analyze') ? true : null,
logStartup: true logStartup: true
} }
Object.assign(config, tryImport(rootDir, './nuxt.config')!.deploy) Object.assign(config, tryImport(rootDir, './nuxt.config')!.deploy)
config.buildDir = resolve(config.rootDir, config.buildDir || '.nuxt') config.buildDir = resolve(config.rootDir, config.buildDir || '.nuxt')
config.targets = config.targets.map(t => typeof t === 'string' ? { target: t } : t)
if (config.target && !config.targets.find(t => t.target === config.target)) {
config.targets.push({ target: config.target })
}
// Ensure dist exists // Ensure dist exists
if (!existsSync(resolve(config.buildDir, 'dist/server'))) { if (!existsSync(resolve(config.buildDir, 'dist/server'))) {
return consola.error('Please use `nuxt build` first to build project!') return consola.error('Please use `nuxt build` first to build project!')
@ -40,10 +48,7 @@ async function main () {
// Compile html template // Compile html template
const htmlTemplateFile = resolve(config.buildDir, `views/${{ 2: 'app', 3: 'document' }[config.nuxt]}.template.html`) const htmlTemplateFile = resolve(config.buildDir, `views/${{ 2: 'app', 3: 'document' }[config.nuxt]}.template.html`)
const htmlTemplateFileJS = htmlTemplateFile.replace(/.html$/, '.js').replace('app.', 'document.') const htmlTemplateFileJS = htmlTemplateFile.replace(/.html$/, '.js').replace('app.', 'document.')
const htmlTemplateContents = await readFile(htmlTemplateFile, 'utf-8') await compileTemplateToJS(htmlTemplateFile, htmlTemplateFileJS)
// eslint-disable-next-line no-template-curly-in-string
const htmlTemplateCompiled = `export default (params) => \`${htmlTemplateContents.replace(/{{ (\w+) }}/g, '${params.$1}')}\``
await writeFile(htmlTemplateFileJS, htmlTemplateCompiled)
consola.info('Generated', prettyPath(htmlTemplateFileJS)) consola.info('Generated', prettyPath(htmlTemplateFileJS))
// Bundle for each target // Bundle for each target
@ -59,9 +64,14 @@ async function main () {
consola.info(`Generating bundle for ${hl(target.target)}`) consola.info(`Generating bundle for ${hl(target.target)}`)
const _config: any = defu( const _config: any = defu(
// Target specific config by user
target, target,
// Global user config
config, config,
tryImport(__dirname, `./targets/${target.target}`) || tryImport(config.rootDir, target.target) // Target defaults
tryImport(__dirname, `./targets/${target.target}`) || tryImport(config.rootDir, target.target),
// Generic defaults
{ outDir: resolve(config.buildDir, `dist/${config.target}`), outName: 'index.js' }
) )
const hooks = new Hookable() const hooks = new Hookable()
@ -69,6 +79,8 @@ async function main () {
await hooks.callHook('config', _config) await hooks.callHook('config', _config)
emptyDir(_config.outDir)
_config.rollupConfig = getRollupConfig(_config) _config.rollupConfig = getRollupConfig(_config)
await hooks.callHook('rollup:before', _config) await hooks.callHook('rollup:before', _config)
const build = await rollup(_config.rollupConfig) const build = await rollup(_config.rollupConfig)
@ -79,6 +91,20 @@ async function main () {
consola.success('Generated', prettyPath((_config.rollupConfig.output as any).file), consola.success('Generated', prettyPath((_config.rollupConfig.output as any).file),
chalk.gray(`(Size: ${size} Gzip: ${zSize})`) chalk.gray(`(Size: ${size} Gzip: ${zSize})`)
) )
for (const tmpl of _config.templates) {
const dstPath = resolve(_config.outDir, tmpl.dst)
await renderTemplate(tmpl.src, dstPath, { config: _config })
consola.info('Compiled', prettyPath(dstPath))
}
if (_config.copyAssets) {
const publicDir = typeof _config.copyAssets === 'string' ? _config.copyAssets : 'public'
const dst = resolve(_config.outDir, publicDir, '_nuxt')
await copy(resolve(_config.buildDir, 'dist/client'), dst)
consola.info('Copied public assets to', prettyPath(dst))
}
await hooks.callHook('done', _config) await hooks.callHook('done', _config)
} }
} }

View File

@ -44,7 +44,7 @@ export const getRollupConfig = (config) => {
const options: RollupConfig = { const options: RollupConfig = {
input: config.entry, input: config.entry,
output: { output: {
file: path.resolve(config.buildDir, `dist/${config.target}`, 'index.js'), file: path.resolve(config.outDir, config.outName),
format: 'cjs', format: 'cjs',
intro: '', intro: '',
outro: '', outro: '',

View File

@ -1,11 +1,11 @@
import { relative } from 'path'
import consola from 'consola' import consola from 'consola'
export default { export default {
entry: require.resolve('./entry'), entry: require.resolve('./entry'),
hooks: { hooks: {
'done' ({ rollupConfig }) { 'done' ({ rollupConfig }) {
consola.info(`Usage: \`node ${relative(process.cwd(), rollupConfig.output.file)} [route]\``) consola.info(`Usage: \`node ${rollupConfig.output.file} [route]\``)
} }
} }
} }

View File

@ -0,0 +1,21 @@
// @ts-ignore
import { render } from '~runtime/server'
addEventListener('fetch', (event: any) => {
const url = new URL(event.request.url)
if (url.pathname.startsWith('/_nuxt') || url.pathname.includes('.') /* is file :} */) {
return
}
event.respondWith(handleEvent(url, event.request))
})
async function handleEvent (url, request) {
try {
const { html, status, headers } = await render(url.pathname, { req: request })
return new Response(html, { status, headers })
} catch (error) {
return new Response('Internal Error: ' + error, { status: 500 })
}
}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<body>
Loading...
<script>
if (!('serviceWorker' in navigator)) {
throw new Error('Browser not supported!')
}
window.addEventListener('load', () => {
navigator.serviceWorker.register('/nuxt.sw.js').then((registration) => {
console.log('ServiceWorker registration successful with scope:', registration.scope)
}).catch(error => {
console.error('ServiceWorker registration failed:', error)
})
})
</script>
</body>
</html>

View File

@ -0,0 +1,29 @@
import { resolve } from 'path'
import consola from 'consola'
export default {
entry: require.resolve('./entry'),
node: false,
copyAssets: '.',
outName: 'nuxt.sw.js',
templates: [
{ src: resolve(__dirname, 'index.html'), dst: 'index.html' },
{ src: resolve(__dirname, 'index.html'), dst: '200.html' }
],
hooks: {
config (config) {
if (config.nuxt === 2) {
config.renderer = 'vue2.basic'
}
},
'rollup:before' ({ rollupConfig }) {
rollupConfig.output.intro =
'const global = {}; const exports = {}; const module = { exports }; const process = { env: {}, hrtime: () => [0,0]};' +
rollupConfig.output.intro
rollupConfig.output.format = 'iife'
},
done ({ outDir }) {
consola.info(`Try with \`npx serve ${outDir}\``)
}
}
}

View File

@ -1,13 +1,34 @@
import { relative } from 'path' import { relative, dirname } from 'path'
import { readFile, writeFile, mkdirp } from 'fs-extra'
import jiti from 'jiti' import jiti from 'jiti'
const pwd = process.cwd() const pwd = process.cwd()
export const hl = str => '`' + str + '`' export const hl = str => '`' + str + '`'
export const prettyPath = (p, highlight = true) => { export function prettyPath (p, highlight = true) {
p = relative(pwd, p) p = relative(pwd, p)
return highlight ? hl(p) : p return highlight ? hl(p) : p
} }
export async function loadTemplate (src) {
const contents = await readFile(src, 'utf-8')
return params => contents.replace(/{{ (\w+) }}/g, `${params.$1}`)
}
export async function renderTemplate (src, dst: string, params: any) {
const tmpl = await loadTemplate(src)
const rendered = tmpl(params)
await mkdirp(dirname(dst))
await writeFile(dst, rendered)
}
export async function compileTemplateToJS (src: string, dst: string) {
const contents = await readFile(src, 'utf-8')
// eslint-disable-next-line no-template-curly-in-string
const compiled = `export default (params) => \`${contents.replace(/{{ (\w+) }}/g, '${params.$1}')}\``
await mkdirp(dirname(dst))
await writeFile(dst, compiled)
}
export const tryImport = (dir, path) => { try { return jiti(dir)(path) } catch (_err) { } } export const tryImport = (dir, path) => { try { return jiti(dir)(path) } catch (_err) { } }