Skip to content

Commit 5ab2d94

Browse files
authored
feat: use a single transport for fetchModule and HMR support (#1711)
1 parent 2019e88 commit 5ab2d94

File tree

2 files changed

+90
-79
lines changed

2 files changed

+90
-79
lines changed

guide/api-environment-instances.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class DevEnvironment {
4141
* ターゲットランタイム内の関連モジュールランナーから
4242
* メッセージを送受信するための通信チャネル。
4343
*/
44-
hot: HotChannel | null
44+
hot: NormalizedHotChannel
4545
/**
4646
* 処理されたモジュールと処理されたコードのキャッシュ結果との間の
4747
* インポートされた関係を示すモジュールノードのグラフ。

guide/api-environment-runtimes.md

Lines changed: 89 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ function createWorkedEnvironment(
2929
dev: {
3030
createEnvironment(name, config) {
3131
return createWorkerdDevEnvironment(name, config, {
32-
hot: customHotChannel(),
32+
hot: true,
33+
transport: customHotChannel(),
3334
})
3435
},
3536
},
@@ -82,29 +83,26 @@ Vite モジュールランナーは、最初に Vite プラグインで処理す
8283
この機能の目的の 1 つは、コードを処理および実行するためのカスタマイズ可能な API を提供することです。ユーザーは、公開されたプリミティブを使用して新しい環境ファクトリーを作成できます。
8384

8485
```ts
85-
import { DevEnvironment, RemoteEnvironmentTransport } from 'vite'
86+
import { DevEnvironment, HotChannel } from 'vite'
8687

8788
function createWorkerdDevEnvironment(
8889
name: string,
8990
config: ResolvedConfig,
9091
context: DevEnvironmentContext
9192
) {
92-
const hot = /* ... */
9393
const connection = /* ... */
94-
const transport = new RemoteEnvironmentTransport({
94+
const transport: HotChannel = {
95+
on: (listener) => { connection.on('message', listener) },
9596
send: (data) => connection.send(data),
96-
onMessage: (listener) => connection.on('message', listener),
97-
})
97+
}
9898

9999
const workerdDevEnvironment = new DevEnvironment(name, config, {
100100
options: {
101101
resolve: { conditions: ['custom'] },
102102
...context.options,
103103
},
104-
hot,
105-
remoteRunner: {
106-
transport,
107-
},
104+
hot: true,
105+
transport,
108106
})
109107
return workerdDevEnvironment
110108
}
@@ -152,13 +150,12 @@ export class ModuleRunner {
152150

153151
```js
154152
import { ModuleRunner, ESModulesEvaluator } from 'vite/module-runner'
155-
import { root, fetchModule } from './rpc-implementation.js'
153+
import { root, transport } from './rpc-implementation.js'
156154

157155
const moduleRunner = new ModuleRunner(
158156
{
159157
root,
160-
fetchModule,
161-
// HMR をサポートするために hmr.connection を提供することもできます
158+
transport,
162159
},
163160
new ESModulesEvaluator(),
164161
)
@@ -177,7 +174,7 @@ export interface ModuleRunnerOptions {
177174
/**
178175
* サーバーと通信するための一連のメソッド。
179176
*/
180-
transport: RunnerTransport
177+
transport: ModuleRunnerTransport
181178
/**
182179
* ソースマップの解決方法を設定します。
183180
* `process.setSourceMapsEnabled` が使用可能な場合は `node` を優先します。
@@ -197,10 +194,6 @@ export interface ModuleRunnerOptions {
197194
hmr?:
198195
| false
199196
| {
200-
/**
201-
* HMR がクライアントとサーバー間で通信する方法を設定します。
202-
*/
203-
connection: ModuleRunnerHMRConnection
204197
/**
205198
* HMR ロガーを設定します。
206199
*/
@@ -245,59 +238,91 @@ export interface ModuleEvaluator {
245238

246239
Vite はデフォルトでこのインターフェイスを実装した `ESModulesEvaluator` をエクスポートします。コードの評価には `new AsyncFunction` を使用するので、インライン化されたソースマップがある場合は、新しい行が追加されたことを考慮して [2 行分のオフセット](https://tc39.es/ecma262/#sec-createdynamicfunction)を追加する必要があります。これは `ESModulesEvaluator` によって自動的に実行されます。カスタムの Evaluator は行を追加しません。
247240

248-
## RunnerTransport
241+
## `ModuleRunnerTransport`
249242

250243
**型シグネチャー:**
251244

252245
```ts
253-
interface RunnerTransport {
254-
/**
255-
* モジュールに関する情報を取得するメソッド。
256-
*/
257-
fetchModule: FetchFunction
246+
interface ModuleRunnerTransport {
247+
connect?(handlers: ModuleRunnerTransportHandlers): Promise<void> | void
248+
disconnect?(): Promise<void> | void
249+
send?(data: HotPayload): Promise<void> | void
250+
invoke?(
251+
data: HotPayload,
252+
): Promise<{ /** result */ r: any } | { /** error */ e: any }>
253+
timeout?: number
258254
}
259255
```
260256

261-
RPC 経由または関数を直接呼び出して環境と通信するトランスポートオブジェクト。デフォルトでは、`fetchModule` メソッドでオブジェクトを渡す必要があります。このメソッド内ではどのようなタイプの RPC も使用できますが、Vite では設定を簡単にするために `RemoteRunnerTransport` クラスを使用して双方向のトランスポートインターフェースを公開しています。モジュールランナーがワーカースレッドで作成される次の例のように、サーバー上の `RemoteEnvironmentTransport` インスタンスと合わせる必要があります:
257+
RPC 経由または関数を直接呼び出して環境と通信するトランスポートオブジェクト。`invoke` メソッドが実装されていない場合、`send` メソッドと `connect` メソッドの実装が必須となります。Vite は内部で `invoke` を構築します。
258+
259+
次の例のように、モジュールランナーがワーカー スレッドで作成されるサーバー上の `HotChannel` インスタンスと結合する必要があります:
262260

263261
::: code-group
264262

265-
```ts [worker.js]
263+
```js [worker.js]
266264
import { parentPort } from 'node:worker_threads'
267265
import { fileURLToPath } from 'node:url'
268-
import {
269-
ESModulesEvaluator,
270-
ModuleRunner,
271-
RemoteRunnerTransport,
272-
} from 'vite/module-runner'
266+
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
267+
268+
/** @type {import('vite/module-runner').ModuleRunnerTransport} */
269+
const transport = {
270+
connect({ onMessage, onDisconnection }) {
271+
parentPort.on('message', onMessage)
272+
parentPort.on('close', onDisconnection)
273+
},
274+
send(data) {
275+
parentPort.postMessage(data)
276+
},
277+
}
273278

274279
const runner = new ModuleRunner(
275280
{
276281
root: fileURLToPath(new URL('./', import.meta.url)),
277-
transport: new RemoteRunnerTransport({
278-
send: (data) => parentPort.postMessage(data),
279-
onMessage: (listener) => parentPort.on('message', listener),
280-
timeout: 5000,
281-
}),
282+
transport,
282283
},
283284
new ESModulesEvaluator(),
284285
)
285286
```
286287
287-
```ts [server.js]
288+
```js [server.js]
288289
import { BroadcastChannel } from 'node:worker_threads'
289290
import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite'
290291

291292
function createWorkerEnvironment(name, config, context) {
292293
const worker = new Worker('./worker.js')
293-
return new DevEnvironment(name, config, {
294-
hot: /* custom hot channel */,
295-
remoteRunner: {
296-
transport: new RemoteEnvironmentTransport({
297-
send: (data) => worker.postMessage(data),
298-
onMessage: (listener) => worker.on('message', listener),
299-
}),
294+
const handlerToWorkerListener = new WeakMap()
295+
296+
const workerHotChannel = {
297+
send: (data) => w.postMessage(data),
298+
on: (event, handler) => {
299+
if (event === 'connection') return
300+
301+
const listener = (value) => {
302+
if (value.type === 'custom' && value.event === event) {
303+
const client = {
304+
send(payload) {
305+
w.postMessage(payload)
306+
},
307+
}
308+
handler(value.data, client)
309+
}
310+
}
311+
handlerToWorkerListener.set(handler, listener)
312+
w.on('message', listener)
313+
},
314+
off: (event, handler) => {
315+
if (event === 'connection') return
316+
const listener = handlerToWorkerListener.get(handler)
317+
if (listener) {
318+
w.off('message', listener)
319+
handlerToWorkerListener.delete(handler)
320+
}
300321
},
322+
}
323+
324+
return new DevEnvironment(name, config, {
325+
transport: workerHotChannel,
301326
})
302327
}
303328

@@ -314,7 +339,7 @@ await createServer({
314339
315340
:::
316341
317-
`RemoteRunnerTransport``RemoteEnvironmentTransport` は一緒に使うことを想定していますが、必ずしも使う必要はありません。独自の関数を定義して、ランナーとサーバー間の通信を行えます。例えば、HTTP リクエストで環境に接続する場合、`fetchModule` 関数で `fetch().json()` を呼び出せます:
342+
HTTP リクエストを使用してランナーとサーバー間で通信する別の例:
318343
319344
```ts
320345
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
@@ -323,10 +348,11 @@ export const runner = new ModuleRunner(
323348
{
324349
root: fileURLToPath(new URL('./', import.meta.url)),
325350
transport: {
326-
async fetchModule(id, importer) {
327-
const response = await fetch(
328-
`http://my-vite-server/fetch?id=${id}&importer=${importer}`,
329-
)
351+
async invoke(data) {
352+
const response = await fetch(`http://my-vite-server/invoke`, {
353+
method: 'POST',
354+
body: JSON.stringify(data),
355+
})
330356
return response.json()
331357
},
332358
},
@@ -337,37 +363,22 @@ export const runner = new ModuleRunner(
337363
await runner.import('/entry.js')
338364
```
339365
340-
## ModuleRunnerHMRConnection
341-
342-
**型シグネチャー:**
366+
この場合、`NormalizedHotChannel``handleInvoke` メソッドを使用できます:
343367
344368
```ts
345-
export interface ModuleRunnerHMRConnection {
346-
/**
347-
* サーバーにメッセージを送信する前にチェックされます。
348-
*/
349-
isReady(): boolean
350-
/**
351-
* サーバーにメッセージを送信します。
352-
*/
353-
send(payload: HotPayload): void
354-
/**
355-
* この接続が更新をトリガーしたときに HMR がどのように処理されるかを設定します。
356-
* このメソッドは、接続が HMR 更新のリッスンを開始し、受信時にこのコールバックを
357-
* 呼び出すことを想定しています。
358-
*/
359-
onUpdate(callback: (payload: HotPayload) => void): void
360-
}
369+
const customEnvironment = new DevEnvironment(name, config, context)
370+
371+
server.onRequest((request: Request) => {
372+
const url = new URL(request.url)
373+
if (url.pathname === '/invoke') {
374+
const payload = (await request.json()) as HotPayload
375+
const result = customEnvironment.hot.handleInvoke(payload)
376+
return new Response(JSON.stringify(result))
377+
}
378+
return Response.error()
379+
})
361380
```
362381
363-
このインターフェイスは HMR 通信の確立方法を定義します。Vite の SSR 中に HMR をサポートするために、Vite は `ServerHMRConnector` をメインエントリーからエクスポートします。`isReady``send` メソッドは通常、カスタムイベントがトリガーされたときに呼び出されます(`import.meta.hot.send("my-event")` のように)。
364-
365-
`onUpdate` は、新しいモジュールランナーが初期化されたときに一度だけ呼ばれます。接続が HMR イベントをトリガーしたときに呼び出されるメソッドを渡します。実装は接続の種類(例として、`WebSocket`/`EventEmitter`/`MessageChannel`)に依存しますが、通常は以下のようになります:
366-
367-
```js
368-
function onUpdate(callback) {
369-
this.connection.on('hmr', (event) => callback(event.data))
370-
}
371-
```
382+
ただし、HMR をサポートするためには `send` メソッドと `connect` メソッドが必要です。`send` メソッドは通常、カスタムイベントがトリガーされたときに呼び出されます(`import.meta.hot.send("my-event")` のように)。
372383
373-
コールバックはキューに入れられ、次の更新を処理する前に現在の更新が解決されるのを待ちます。ブラウザーの実装とは異なり、モジュールランナーにおける HMR の更新は、モジュールを更新する前に、すべてのリスナー(`vite:beforeUpdate`/`vite:beforeFullReload` など)が終了するまで待機します
384+
Vite は SSR 中の HMR をサポートするために、メインエントリーポイントから `createServerHotChannel` をエクスポートします

0 commit comments

Comments
 (0)