Skip to content

Commit

Permalink
feat(dev): expose APIs for client-server communication (#7437)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Mar 26, 2022
1 parent 0d7ef43 commit e29ea8e
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 23 deletions.
8 changes: 8 additions & 0 deletions docs/guide/api-hmr.md
Expand Up @@ -123,3 +123,11 @@ The following HMR events are dispatched by Vite automatically:
- `'vite:error'` when an error occurs (e.g. syntax error)
Custom HMR events can also be sent from plugins. See [handleHotUpdate](./api-plugin#handlehotupdate) for more details.
## `hot.send(event, data)`
Send custom events back to Vite's dev server.
If called before connected, the data will be buffered and sent once the connection is established.
See [Client-server Communication](/guide/api-plugin.html#client-server-communication) for more details.
70 changes: 69 additions & 1 deletion docs/guide/api-plugin.md
Expand Up @@ -480,7 +480,7 @@ export default defineConfig({
Check out [Vite Rollup Plugins](https://vite-rollup-plugins.patak.dev) for a list of compatible official Rollup plugins with usage instructions.
## Path normalization
## Path Normalization
Vite normalizes paths while resolving ids to use POSIX separators ( / ) while preserving the volume in Windows. On the other hand, Rollup keeps resolved paths untouched by default, so resolved ids have win32 separators ( \\ ) in Windows. However, Rollup plugins use a [`normalizePath` utility function](https://github.com/rollup/plugins/tree/master/packages/pluginutils#normalizepath) from `@rollup/pluginutils` internally, which converts separators to POSIX before performing comparisons. This means that when these plugins are used in Vite, the `include` and `exclude` config pattern and other similar paths against resolved ids comparisons work correctly.
Expand All @@ -492,3 +492,71 @@ import { normalizePath } from 'vite'
normalizePath('foo\\bar') // 'foo/bar'
normalizePath('foo/bar') // 'foo/bar'
```
## Client-server Communication
Since Vite 2.9, we provide some utilities for plugins to help handle the communication with clients.
### Server to Client
On the plugin side, we could use `server.ws.send` to boardcast events to all the clients:
```js
// vite.config.js
export default defineConfig({
plugins: [
{
// ...
configureServer(server) {
server.ws.send('my:greetings', { msg: 'hello' })
}
}
]
})
```
::: tip NOTE
We recommend **alway prefixing** your event names to avoid collisions with other plugins.
:::
On the client side, use [`hot.on`](/guide/api-hmr.html#hot-on-event-cb) to listen to the events:
```ts
// client side
if (import.meta.hot) {
import.meta.hot.on('my:greetings', (data) => {
console.log(data.msg) // hello
})
}
```
### Client to Server
To send events from the client to the server, we can use [`hot.send`](/guide/api-hmr.html#hot-send-event-payload):
```ts
// client side
if (import.meta.hot) {
import.meta.hot.send('my:from-client', { msg: 'Hey!' })
}
```
Then use `server.ws.on` and listen to the events on the server side:
```js
// vite.config.js
export default defineConfig({
plugins: [
{
// ...
configureServer(server) {
server.ws.on('my:from-client', (data, client) => {
console.log('Message from client:', data.msg) // Hey!
// reply only to the client (if needed)
client.send('my:ack', { msg: 'Hi! I got your message!' })
})
}
}
]
})
```
7 changes: 6 additions & 1 deletion packages/playground/hmr/__tests__/hmr.spec.ts
Expand Up @@ -123,11 +123,16 @@ if (!isBuild) {
await untilUpdated(() => el.textContent(), 'edited')
})

test('plugin client-server communication', async () => {
const el = await page.$('.custom-communication')
await untilUpdated(() => el.textContent(), '3')
})

test('full-reload encodeURI path', async () => {
await page.goto(
viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html'
)
let el = await page.$('#app')
const el = await page.$('#app')
expect(await el.textContent()).toBe('title')
await editFile(
'unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html',
Expand Down
6 changes: 6 additions & 0 deletions packages/playground/hmr/hmr.js
Expand Up @@ -57,6 +57,12 @@ if (import.meta.hot) {
import.meta.hot.on('foo', ({ msg }) => {
text('.custom', msg)
})

// send custom event to server to calculate 1 + 2
import.meta.hot.send('remote-add', { a: 1, b: 2 })
import.meta.hot.on('remote-add-result', ({ result }) => {
text('.custom-communication', result)
})
}

function text(el, text) {
Expand Down
1 change: 1 addition & 0 deletions packages/playground/hmr/index.html
Expand Up @@ -5,5 +5,6 @@
<div class="dep"></div>
<div class="nested"></div>
<div class="custom"></div>
<div class="custom-communication"></div>
<div class="css-prev"></div>
<div class="css-post"></div>
13 changes: 6 additions & 7 deletions packages/playground/hmr/vite.config.js
Expand Up @@ -9,14 +9,13 @@ module.exports = {
if (file.endsWith('customFile.js')) {
const content = await read()
const msg = content.match(/export const msg = '(\w+)'/)[1]
server.ws.send({
type: 'custom',
event: 'foo',
data: {
msg
}
})
server.ws.send('foo', { msg })
}
},
configureServer(server) {
server.ws.on('remote-add', ({ a, b }, client) => {
client.send('remote-add-result', { result: a + b })
})
}
}
]
Expand Down
16 changes: 15 additions & 1 deletion packages/vite/src/client/client.ts
Expand Up @@ -30,6 +30,7 @@ const socketHost = __HMR_PORT__

const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')
const base = __BASE__ || '/'
const messageBuffer: string[] = []

function warnFailedFetch(err: Error, path: string | string[]) {
if (!err.message.match('fetch')) {
Expand Down Expand Up @@ -59,9 +60,10 @@ async function handleMessage(payload: HMRPayload) {
switch (payload.type) {
case 'connected':
console.log(`[vite] connected.`)
sendMessageBuffer()
// proxy(nginx, docker) hmr ws maybe caused timeout,
// so send ping package let ws keep alive.
setInterval(() => socket.send('ping'), __HMR_TIMEOUT__)
setInterval(() => socket.send('{"type":"ping"}'), __HMR_TIMEOUT__)
break
case 'update':
notifyListeners('vite:beforeUpdate', payload)
Expand Down Expand Up @@ -361,6 +363,13 @@ async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
}
}

function sendMessageBuffer() {
if (socket.readyState === 1) {
messageBuffer.forEach((msg) => socket.send(msg))
messageBuffer.length = 0
}
}

interface HotModule {
id: string
callbacks: HotCallback[]
Expand Down Expand Up @@ -478,6 +487,11 @@ export const createHotContext = (ownerPath: string) => {
}
addToMap(customListenersMap)
addToMap(newListeners)
},

send: (event: string, data?: any) => {
messageBuffer.push(JSON.stringify({ type: 'custom', event, data }))
sendMessageBuffer()
}
}

Expand Down
6 changes: 5 additions & 1 deletion packages/vite/src/node/index.ts
Expand Up @@ -73,7 +73,11 @@ export type { TransformOptions as EsbuildTransformOptions } from 'esbuild'
export type { ESBuildOptions, ESBuildTransformResult } from './plugins/esbuild'
export type { Manifest, ManifestChunk } from './plugins/manifest'
export type { ResolveOptions, InternalResolveOptions } from './plugins/resolve'
export type { WebSocketServer } from './server/ws'
export type {
WebSocketServer,
WebSocketClient,
WebSocketCustomListener
} from './server/ws'
export type { PluginContainer } from './server/pluginContainer'
export type { ModuleGraph, ModuleNode, ResolvedUrl } from './server/moduleGraph'
export type { SendOptions } from './server/send'
Expand Down

0 comments on commit e29ea8e

Please sign in to comment.