From ecc7219786e363988976f15d9223565a34a673eb Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 26 Apr 2020 22:24:29 -0400 Subject: [PATCH] feat: http caching for vue requests --- src/node/serverPluginHmr.ts | 2 +- src/node/serverPluginModules.ts | 20 ++++++------- src/node/serverPluginVue.ts | 52 +++++++++++++++++++++++---------- src/node/utils.ts | 29 +++++++++++++----- 4 files changed, 69 insertions(+), 34 deletions(-) diff --git a/src/node/serverPluginHmr.ts b/src/node/serverPluginHmr.ts index fc7c5337d1fb7a..e800f1996080c4 100644 --- a/src/node/serverPluginHmr.ts +++ b/src/node/serverPluginHmr.ts @@ -59,7 +59,7 @@ export const hmrPlugin: Plugin = ({ root, app, server, watcher, resolver }) => { } debug('serving hmr client') ctx.type = 'js' - ctx.body = await cachedRead(hmrClientFilePath) + await cachedRead(ctx, hmrClientFilePath) }) // start a websocket server to send hmr notifications to the client diff --git a/src/node/serverPluginModules.ts b/src/node/serverPluginModules.ts index cc3812d6e09c6f..f009cefaa9721e 100644 --- a/src/node/serverPluginModules.ts +++ b/src/node/serverPluginModules.ts @@ -21,13 +21,13 @@ const fileToIdMap = new Map() const webModulesMap = new Map() const rewriteCache = new LRUCache({ max: 65535 }) -export const modulesPlugin: Plugin = ({ root, app, watcher }) => { +export const modulesPlugin: Plugin = ({ root, app, watcher, resolver }) => { // bust module rewrite cache on file change watcher.on('change', (file) => { - // TODO also need logic for reverse mapping file to servedPath - const servedPath = '/' + path.relative(root, file) - debugImportRewrite(`${servedPath}: cache busted`) - rewriteCache.del(servedPath) + // TODO also need logic for reverse mapping file to publicPath + const publicPath = resolver.fileToPublic(file) + debugImportRewrite(`${publicPath}: cache busted`) + rewriteCache.del(publicPath) }) // rewrite named module imports to `/@modules/:id` requests @@ -99,7 +99,7 @@ export const modulesPlugin: Plugin = ({ root, app, watcher }) => { // special handling for vue's runtime. if (id === 'vue') { const vuePath = resolveVue(root).vue - ctx.body = await cachedRead(vuePath) + await cachedRead(ctx, vuePath) debugModuleResolution(`vue -> ${getDebugPath(vuePath)}`) return } @@ -107,8 +107,8 @@ export const modulesPlugin: Plugin = ({ root, app, watcher }) => { // already resolved and cached const cachedPath = idToFileMap.get(id) if (cachedPath) { + await cachedRead(ctx, cachedPath) debugModuleResolution(`(cached) ${id} -> ${getDebugPath(cachedPath)}`) - ctx.body = await cachedRead(cachedPath) return } @@ -135,8 +135,8 @@ export const modulesPlugin: Plugin = ({ root, app, watcher }) => { path.basename(sourceMapRequest) ) idToFileMap.set(sourceMapRequest, sourceMapPath) + await cachedRead(ctx, sourceMapPath) ctx.type = 'application/json' - ctx.body = await cachedRead(sourceMapPath) debugModuleResolution( `(source map) ${id} -> ${getDebugPath(sourceMapPath)}` ) @@ -149,7 +149,7 @@ export const modulesPlugin: Plugin = ({ root, app, watcher }) => { if (webModulePath) { idToFileMap.set(id, webModulePath) fileToIdMap.set(path.basename(webModulePath), id) - ctx.body = await cachedRead(webModulePath) + await cachedRead(ctx, webModulePath) debugModuleResolution(`${id} -> ${getDebugPath(webModulePath)}`) return } @@ -179,7 +179,7 @@ export const modulesPlugin: Plugin = ({ root, app, watcher }) => { idToFileMap.set(id, modulePath) fileToIdMap.set(path.basename(modulePath), id) debugModuleResolution(`${id} -> ${getDebugPath(modulePath)}`) - ctx.body = await cachedRead(modulePath) + await cachedRead(ctx, modulePath) } catch (e) { console.error( chalk.red(`[vite] Error while resolving node_modules with id "${id}":`) diff --git a/src/node/serverPluginVue.ts b/src/node/serverPluginVue.ts index 1aaa1922fc4025..4bf6485fc88a4b 100644 --- a/src/node/serverPluginVue.ts +++ b/src/node/serverPluginVue.ts @@ -1,3 +1,4 @@ +import { promises as fs } from 'fs' import { Plugin } from './server' import { SFCDescriptor, @@ -7,11 +8,13 @@ import { } from '@vue/compiler-sfc' import { resolveCompiler } from './resolveVue' import hash_sum from 'hash-sum' -import { cachedRead } from './utils' import LRUCache from 'lru-cache' import { hmrClientPublicPath } from './serverPluginHmr' import resolve from 'resolve-from' +import { Context } from 'koa' +import { cachedRead } from './utils' +const getEtag = require('etag') const debug = require('debug')('vite:sfc') interface CacheEntry { @@ -34,7 +37,10 @@ export const vuePlugin: Plugin = ({ root, app, resolver }) => { const query = ctx.query const publicPath = ctx.path const filePath = resolver.publicToFile(publicPath) - const descriptor = await parseSFC(root, filePath) + const timestamp = query.t + + await cachedRead(ctx, filePath) + const descriptor = await parseSFC(root, filePath, ctx.body) if (!descriptor) { debug(`${ctx.url} - 404`) @@ -44,23 +50,20 @@ export const vuePlugin: Plugin = ({ root, app, resolver }) => { if (!query.type) { ctx.type = 'js' - ctx.body = compileSFCMain( - descriptor, - filePath, - publicPath, - query.t as string - ) + ctx.body = compileSFCMain(descriptor, filePath, publicPath, timestamp) return } if (query.type === 'template') { ctx.type = 'js' ctx.body = compileSFCTemplate( + ctx, root, descriptor.template!, filePath, publicPath, - descriptor.styles.some((s) => s.scoped) + descriptor.styles.some((s) => s.scoped), + timestamp ) return } @@ -69,11 +72,13 @@ export const vuePlugin: Plugin = ({ root, app, resolver }) => { const index = Number(query.index) const styleBlock = descriptor.styles[index] const result = await compileSFCStyle( + ctx, root, styleBlock, index, filePath, - publicPath + publicPath, + timestamp ) if (query.module != null) { ctx.type = 'js' @@ -101,7 +106,7 @@ export async function parseSFC( if (!content) { try { - content = await cachedRead(filename, 'utf-8') + content = await fs.readFile(filename, 'utf-8') } catch (e) { return } @@ -191,17 +196,25 @@ function compileSFCMain( } function compileSFCTemplate( + ctx: Context, root: string, template: SFCTemplateBlock, filename: string, pathname: string, - scoped: boolean + scoped: boolean, + timestamp: string | undefined ): string { let cached = vueCache.get(filename) if (cached && cached.template) { + if (timestamp) { + ctx.status = 200 + } else { + ctx.etag = getEtag(cached.template) + } return cached.template } + ctx.status = 200 const { code, errors } = resolveCompiler(root).compileTemplate({ source: template.content, filename, @@ -222,17 +235,26 @@ function compileSFCTemplate( } async function compileSFCStyle( + ctx: Context, root: string, style: SFCStyleBlock, index: number, filename: string, - pathname: string + pathname: string, + timestamp: string | undefined ): Promise { let cached = vueCache.get(filename) - if (cached && cached.styles && cached.styles[index]) { - return cached.styles[index] + const cachedEntry = cached && cached.styles && cached.styles[index] + if (cachedEntry) { + if (timestamp) { + ctx.status = 200 + } else { + ctx.etag = getEtag(cachedEntry.code) + } + return cachedEntry } + ctx.status = 200 const id = hash_sum(pathname) const result = await resolveCompiler(root).compileStyleAsync({ source: style.content, diff --git a/src/node/utils.ts b/src/node/utils.ts index a92f6dfa97b980..b6d855051c25a2 100644 --- a/src/node/utils.ts +++ b/src/node/utils.ts @@ -1,9 +1,14 @@ +import path from 'path' import { promises as fs } from 'fs' import LRUCache from 'lru-cache' import os from 'os' +import { Context } from 'koa' + +const getETag = require('etag') interface CacheEntry { lastModified: number + etag: string content: Buffer | string } @@ -11,20 +16,28 @@ const moduleReadCache = new LRUCache({ max: 10000 }) -export function cachedRead(path: string): Promise -export function cachedRead(path: string, encoding: string): Promise -export async function cachedRead(path: string, encoding?: string) { - const lastModified = (await fs.stat(path)).mtimeMs - const cached = moduleReadCache.get(path) +export async function cachedRead(ctx: Context, file: string) { + const lastModified = (await fs.stat(file)).mtimeMs + const cached = moduleReadCache.get(file) + ctx.set('Cache-Control', 'no-cache') + ctx.type = path.basename(file) if (cached && cached.lastModified === lastModified) { + ctx.etag = cached.etag + ctx.lastModified = new Date(cached.lastModified) + ctx.status = 304 return cached.content } - const content = await fs.readFile(path, encoding) - moduleReadCache.set(path, { + const content = await fs.readFile(file, 'utf-8') + const etag = getETag(content) + moduleReadCache.set(file, { content, + etag, lastModified }) - return content + ctx.etag = etag + ctx.lastModified = new Date(lastModified) + ctx.body = content + ctx.status = 200 } export function getIPv4AddressList(): string[] {