Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugins/worker): support SharedWorker (resolve #2093) #2505

Merged
merged 3 commits into from
May 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import shaderString from './shader.glsl?raw'

### Importing Script as a Worker

Scripts can be imported as web workers with the `?worker` suffix.
Scripts can be imported as web workers with the `?worker` or `?sharedworker` suffix.

```js
// Separate chunk in the production build
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ In the production build, `.wasm` files smaller than `assetInlineLimit` will be i

## Web Workers

A web worker script can be directly imported by appending `?worker` to the import request. The default export will be a custom worker constructor:
A web worker script can be directly imported by appending `?worker` or `?sharedworker` to the import request. The default export will be a custom worker constructor:

```js
import MyWorker from './worker?worker'
Expand Down
30 changes: 28 additions & 2 deletions packages/playground/worker/__tests__/worker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs'
import path from 'path'
import { untilUpdated, isBuild, testDir } from '../../testUtils'
import { Page } from 'playwright-chromium'

test('normal', async () => {
await page.click('.ping')
Expand All @@ -16,17 +17,42 @@ test('inlined', async () => {
await untilUpdated(() => page.textContent('.pong-inline'), 'pong')
})

const waitSharedWorkerTick = (
(resolvedSharedWorkerCount: number) => async (page: Page) => {
await untilUpdated(async () => {
const count = await page.textContent('.tick-count')
// ignore the initial 0
return count === '1' ? 'page loaded' : ''
}, 'page loaded')
// test.concurrent sequential is not guaranteed
// force page to wait to ensure two pages overlap in time
resolvedSharedWorkerCount++

await untilUpdated(() => {
return resolvedSharedWorkerCount === 2 ? 'all pages loaded' : ''
}, 'all pages loaded')
}
)(0)

test.concurrent.each([[true], [false]])('shared worker', async (doTick) => {
if (doTick) {
await page.click('.tick-shared')
}
await waitSharedWorkerTick(page)
})

if (isBuild) {
// assert correct files
test('inlined code generation', async () => {
const assetsDir = path.resolve(testDir, 'dist/assets')
const files = fs.readdirSync(assetsDir)
// should have only 1 worker chunk
expect(files.length).toBe(2)
// should have 2 worker chunk
expect(files.length).toBe(3)
const index = files.find((f) => f.includes('index'))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
// chunk
expect(content).toMatch(`new Worker("/assets`)
expect(content).toMatch(`new SharedWorker("/assets`)
// inlined
expect(content).toMatch(`(window.URL||window.webkitURL).createObjectURL`)
})
Expand Down
18 changes: 18 additions & 0 deletions packages/playground/worker/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@
<button class="ping-inline">Ping Inline Worker</button>
<div>Response from inline worker: <span class="pong-inline"></span></div>

<button class="tick-shared">Tick Shared Worker</button>
<div>
Tick from shared worker, it syncs between pages:
<span class="tick-count">0</span>
</div>

<script type="module">
import Worker from './my-worker?worker'
import InlineWorker from './my-worker?worker&inline'
import SharedWorker from './my-shared-worker?sharedworker&name=shared'

const worker = new Worker()
worker.addEventListener('message', (e) => {
Expand All @@ -28,4 +35,15 @@
document.querySelector('.ping-inline').addEventListener('click', () => {
inlineWorker.postMessage('ping')
})

const sharedWorker = new SharedWorker()
document.querySelector('.tick-shared').addEventListener('click', () => {
sharedWorker.port.postMessage('tick')
})

sharedWorker.port.addEventListener('message', (event) => {
document.querySelector('.tick-count').textContent = event.data
})

sharedWorker.port.start()
</script>
16 changes: 16 additions & 0 deletions packages/playground/worker/my-shared-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
let count = 0
const ports = new Set()

onconnect = (event) => {
const port = event.ports[0]
ports.add(port)
port.postMessage(count)
port.onmessage = (message) => {
if (message.data === 'tick') {
count++
ports.forEach((p) => {
p.postMessage(count)
})
}
}
}
2 changes: 1 addition & 1 deletion packages/vite/src/node/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const JS_TYPES_RE = /\.(?:j|t)sx?$|\.mjs$/

export const OPTIMIZABLE_ENTRY_RE = /\.(?:m?js|ts)$/

export const SPECIAL_QUERY_RE = /[\?&](?:worker|raw|url)\b/
export const SPECIAL_QUERY_RE = /[\?&](?:worker|sharedworker|raw|url)\b/

/**
* Prefix for resolved fs paths, since windows paths may not be valid as URLs.
Expand Down
23 changes: 19 additions & 4 deletions packages/vite/src/node/plugins/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
name: 'vite:worker',

load(id) {
if (isBuild && parseWorkerRequest(id)?.worker != null) {
return ''
if (isBuild) {
const parsedQuery = parseWorkerRequest(id)
if (
parsedQuery &&
(parsedQuery.worker ?? parsedQuery.sharedworker) != null
) {
return ''
}
}
},

Expand All @@ -36,7 +42,10 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
code: `import '${ENV_PUBLIC_PATH}'\n` + _
}
}
if (query == null || (query && query.worker == null)) {
if (
query == null ||
(query && (query.worker ?? query.sharedworker) == null)
) {
return
}

Expand Down Expand Up @@ -80,8 +89,14 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
url = injectQuery(url, WorkerFileId)
}

const workerConstructor =
query.sharedworker != null ? 'SharedWorker' : 'Worker'
const workerOptions = { type: 'module' }

return `export default function WorkerWrapper() {
return new Worker(${JSON.stringify(url)}, { type: 'module' })
return new ${workerConstructor}(${JSON.stringify(
url
)}, ${JSON.stringify(workerOptions, null, 2)})
}`
}
}
Expand Down