Skip to content

Commit

Permalink
feat: service worker caching
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed May 12, 2020
1 parent 3dc39fa commit ee6a03d
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 38 deletions.
10 changes: 3 additions & 7 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ socket.addEventListener('message', async ({ data }) => {
if (changeSrcPath) {
await bustSwCache(changeSrcPath)
}
if (path !== changeSrcPath) {
await bustSwCache(path)
}

switch (type) {
case 'connected':
Expand Down Expand Up @@ -108,14 +111,7 @@ socket.addEventListener('message', async ({ data }) => {
cbs.forEach((cb) => cb(customData))
}
break
case 'sw-bust-cache':
// this is only called on file deletion
bustSwCache(path)
break
case 'full-reload':
// make sure to bust the cache for the file that changed before
// reloading the page!
await bustSwCache(path)
location.reload()
}
})
Expand Down
7 changes: 6 additions & 1 deletion src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ Options:
--help, -h [boolean] show help
--version, -v [boolean] show version
--config, -c [string] use specified config file
--serviceWorker, -sw [boolean | 'deps-only'] configure service worker caching behavior
--port [number] port to use for serve
--open [boolean] open browser on server start
--base [string] public base path for build (default: /)
--outDir [string] output directory for build (default: dist)
--assetsDir [string] directory under outDir to place assets in (default: assets)
--assetsInlineLimit [number] static asset base64 inline threshold in bytes (default: 4096)
--sourcemap [boolean] output source maps for build (default: false)
--minify [boolean | 'terser' | 'esbuild'] disable minification, or specify
--minify [boolean | 'terser' | 'esbuild'] enable/disable minification, or specify
minifier to use. (default: 'terser')
--ssr [boolean] build for server-side rendering
--jsx ['vue' | 'preact' | 'react'] choose jsx preset (default: 'vue')
Expand Down Expand Up @@ -58,6 +59,10 @@ console.log(chalk.cyan(`vite v${require('../package.json').version}`))
})()

async function resolveOptions() {
// shorthand for serviceWorker option
if (argv['sw']) {
argv.serviceWorker = argv['sw']
}
// map jsx args
if (argv['jsx-factory']) {
;(argv.jsx || (argv.jsx = {})).factory = argv['jsx-factory']
Expand Down
13 changes: 12 additions & 1 deletion src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ export interface SharedConfig {
}

export interface ServerConfig extends SharedConfig {
/**
* Whether to use a Service Worker to cache served code. This greatly improves
* full page reload performance. Set to false to disable so that every
* request will hit the server (returns 304 if file didn't change), or set
* to 'deps-only' so that it only caches 3rd party dependencies.
*
* @default true
*/
serviceWorker?: boolean | 'deps-only'
plugins?: ServerPlugin[]
}

Expand Down Expand Up @@ -142,7 +151,9 @@ export interface BuildConfig extends SharedConfig {
emitAssets?: boolean
}

export interface UserConfig extends BuildConfig {
export interface UserConfig
extends BuildConfig,
Pick<ServerConfig, 'serviceWorker'> {
plugins?: Plugin[]
configureServer?: ServerPlugin
}
Expand Down
5 changes: 5 additions & 0 deletions src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,17 @@ export function createServer(config: ServerConfig = {}): Server {
transforms = []
} = config

if (config.serviceWorker == null) {
config.serviceWorker = true
}

const app = new Koa()
const server = http.createServer(app.callback())
const watcher = chokidar.watch(root, {
ignored: [/node_modules/]
}) as HMRWatcher
const resolver = createResolver(root, resolvers, alias)

const context = {
root,
app,
Expand Down
13 changes: 8 additions & 5 deletions src/node/server/serverPluginHmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@
// updates using the full paths of the dependencies.

import { ServerPlugin } from '.'
import fs from 'fs'
import WebSocket from 'ws'
import path from 'path'
import chalk from 'chalk'
import hash_sum from 'hash-sum'
import { SFCBlock } from '@vue/compiler-sfc'
import { parseSFC, vueCache, srcImportMap } from './serverPluginVue'
import { resolveImport } from './serverPluginModuleRewrite'
import { cachedRead } from '../utils'
import { FSWatcher } from 'chokidar'
import MagicString from 'magic-string'
import { parse } from '@babel/parser'
Expand Down Expand Up @@ -97,13 +97,16 @@ export const hmrPlugin: ServerPlugin = ({
resolver,
config
}) => {
const hmrClient = fs.readFileSync(hmrClientFilePath, 'utf-8')

app.use(async (ctx, next) => {
if (ctx.path !== hmrClientPublicPath) {
if (ctx.path === hmrClientPublicPath) {
ctx.type = 'js'
ctx.status = 200
ctx.body = hmrClient
} else {
return next()
}
debugHmr('serving hmr client')
ctx.type = 'js'
await cachedRead(ctx, hmrClientFilePath)
})

// start a websocket server to send hmr notifications to the client
Expand Down
13 changes: 10 additions & 3 deletions src/node/server/serverPluginServeStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { ServerPlugin } from '.'
const send = require('koa-send')
const debug = require('debug')('vite:history')

export const serveStaticPlugin: ServerPlugin = ({ root, app, resolver }) => {
export const serveStaticPlugin: ServerPlugin = ({
root,
app,
resolver,
config
}) => {
app.use((ctx, next) => {
// short circuit requests that have already been explicitly handled
if (ctx.body || ctx.status !== 404) {
Expand Down Expand Up @@ -50,8 +55,10 @@ export const serveStaticPlugin: ServerPlugin = ({ root, app, resolver }) => {
return next()
})

app.use(require('koa-conditional-get')())
app.use(require('koa-etag')())
if (config.serviceWorker !== true) {
app.use(require('koa-conditional-get')())
app.use(require('koa-etag')())
}

app.use((ctx, next) => {
const redirect = resolver.requestToFile(ctx.path)
Expand Down
22 changes: 18 additions & 4 deletions src/node/server/serverPluginServiceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,33 @@ import path from 'path'
import { ServerPlugin } from '.'

// TODO inject lockfile hash
// TODO use file content / lastModified hash instead of timestamp?

export const serviceWorkerPlugin: ServerPlugin = ({
root,
app,
watcher,
resolver
resolver,
config
}) => {
const enabledString =
typeof config.serviceWorker === 'boolean'
? String(config.serviceWorker)
: JSON.stringify(config.serviceWorker)

let swScript = fs
.readFileSync(path.resolve(__dirname, '../serviceWorker.js'), 'utf-8')
.replace(/const __ENABLED__ =.*/, `const __ENABLED__ = ${enabledString}`)
// make sure the sw cache is unique per project
.replace(
/__PROJECT_ROOT__ =.*/,
`__PROJECT_ROOT__ = ${JSON.stringify(root)}`
/const __PROJECT_ROOT__ =.*/,
`const __PROJECT_ROOT__ = ${JSON.stringify(root)}`
)
// inject server start time so the sw cache is invalidated
.replace(/__SERVER_TIMESTAMP__ =.*/, `__SERVER_TIMESTAMP__ = ${Date.now()}`)
.replace(
/const __SERVER_TIMESTAMP__ =.*/,
`const __SERVER_TIMESTAMP__ = ${Date.now()}`
)

// enable console logs in debug mode
if (process.env.DEBUG === 'vite:sw') {
Expand All @@ -38,6 +49,9 @@ export const serviceWorkerPlugin: ServerPlugin = ({
// - notify the client to update the sw

app.use(async (ctx, next) => {
// expose config to cachedRead
ctx.__serviceWorker = config.serviceWorker

if (ctx.path === '/sw.js') {
ctx.type = 'js'
ctx.status = 200
Expand Down
13 changes: 8 additions & 5 deletions src/node/server/serverPluginVue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,21 @@ export const vueCache = new LRUCache<string, CacheEntry>({
max: 65535
})

const etagCacheCheck = (ctx: Context) => {
ctx.etag = getEtag(ctx.body)
ctx.status = ctx.etag === ctx.get('If-None-Match') ? 304 : 200
}

export const vuePlugin: ServerPlugin = ({
root,
app,
resolver,
watcher,
config
}) => {
const etagCacheCheck = (ctx: Context) => {
// only add 304 tag check if not using service worker to cache user code
if (config.serviceWorker !== true) {
ctx.etag = getEtag(ctx.body)
ctx.status = ctx.etag === ctx.get('If-None-Match') ? 304 : 200
}
}

app.use(async (ctx, next) => {
if (!ctx.path.endsWith('.vue') && !ctx.vue) {
return next()
Expand Down
11 changes: 6 additions & 5 deletions src/node/utils/fsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ export async function cachedRead(
}
if (cached && cached.lastModified === lastModified) {
if (ctx) {
ctx.etag = cached.etag
ctx.lastModified = new Date(cached.lastModified)
if (ctx.get('If-None-Match') === ctx.etag) {
ctx.status = 304
if (ctx.__serviceWorker !== true) {
ctx.etag = cached.etag
if (ctx.get('If-None-Match') === ctx.etag) {
ctx.status = 304
}
}
// still set the content for *.vue requests
ctx.lastModified = new Date(cached.lastModified)
ctx.body = cached.content
}
return cached.content
Expand Down
9 changes: 9 additions & 0 deletions src/sw/serviceWorker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// These are injected by the server on the fly so that we invalidate the cache.
const __ENABLED__ = true as boolean | 'deps-only'
const __PROJECT_ROOT__ = '/'
const __SERVER_TIMESTAMP__ = 1
const __LOCKFILE_HASH__ = 'a'
Expand Down Expand Up @@ -51,7 +52,15 @@ const depsRE = /^\/@modules\//
const hmrRequestRE = /(&|\?)t=\d+/

sw.addEventListener('fetch', (e) => {
if (!__ENABLED__) {
return
}

const url = new URL(e.request.url)
if (__ENABLED__ === 'deps-only' && !url.pathname.startsWith(`/@modules/`)) {
return
}

if (
// cacheableRequestRE.test(url.pathname) &&
// no need to cache hmr update requests
Expand Down
38 changes: 31 additions & 7 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ describe('vite', () => {
await expectByPolling(() => getText('.src-imports-script'), 'bye from')
// template
await updateFile('src-import/template.html', (c) =>
c.replace('{{ msg }}', 'changed')
c.replace('{{ msg }}', '{{ msg }} changed')
)
await expectByPolling(() => getText('.src-imports-script'), 'changed')
}
Expand Down Expand Up @@ -317,18 +317,18 @@ describe('vite', () => {
// test build first since we are going to edit the fixtures when testing dev
describe('build', () => {
let staticServer
afterAll(() => {
if (staticServer) staticServer.close()
})

test('should build without error', async () => {
beforeAll(async () => {
const buildOutput = await execa(binPath, ['build'], {
cwd: tempDir
})
expect(buildOutput.stdout).toMatch('Build completed')
expect(buildOutput.stderr).toBe('')
})

afterAll(() => {
if (staticServer) staticServer.close()
})

describe('assertions', () => {
beforeAll(async () => {
// start a static file server
Expand Down Expand Up @@ -369,6 +369,30 @@ describe('vite', () => {
})

declareTests(false)

// Assert that all edited files are reflected on page reload
// i.e. service-worker cache is correctly busted
test('sw cache busting', async () => {
await page.reload()

expect(await getText('.hmr-increment')).toMatch('>>> count is 1337 <<<')
expect(await getText('.hmr-propagation')).toMatch('666')
expect(await getComputedColor('.postcss-from-css')).toBe('rgb(0, 128, 0)')
expect(await getComputedColor('.postcss-from-sfc')).toBe('rgb(255, 0, 0)')
expect(await getComputedColor('.style-scoped')).toBe('rgb(0, 0, 0)')
expect(await getComputedColor('.css-modules-sfc')).toBe('rgb(0, 0, 0)')
expect(await getComputedColor('.css-modules-import')).toBe('rgb(0, 0, 1)')
expect(await getComputedColor('.pug')).toBe('rgb(0, 0, 0)')
expect(await getText('.pug')).toMatch('pug with hmr')
expect(await getComputedColor('.src-imports-style')).toBe('rgb(0, 0, 0)')
expect(await getText('.src-imports-script')).toMatch('bye from')
expect(await getText('.src-imports-script')).toMatch('changed')
expect(await getText('.jsx-root')).toMatch('2046')
expect(await getText('.alias')).toMatch('alias hmr works')
expect(await getComputedColor('.transform-scss')).toBe('rgb(0, 0, 0)')
expect(await getText('.transform-js')).toMatch('3')
expect(await getText('.json')).toMatch('with hmr')
})
})
})

Expand All @@ -380,7 +404,7 @@ async function updateFile(file, replacer) {

// poll until it updates
async function expectByPolling(poll, expected) {
const maxTries = 10
const maxTries = 20
for (let tries = 0; tries < maxTries; tries++) {
const actual = await poll()
if (actual.indexOf(expected) > -1 || tries === maxTries - 1) {
Expand Down

0 comments on commit ee6a03d

Please sign in to comment.