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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dev): expose APIs for client-server communication #7437

Merged
merged 11 commits into from Mar 26, 2022
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