partytown의 워커 동기화를 위한 주 루틴 조작을 시도해 보십시오

68527 단어 workerpartytowntech

Partytown


GitHub - BuilderIO/partytown: Relocate resource intensive third-party scripts off of the main thread and into a web worker. 🎉
지금 파티타운이 엉망이야.JavaScript Sandbox의 미래는 어디입니까?
주로 제3party script를 안전하게 격리하기 위해 WebWorker+DOM의 모크로 이동합니다.
GitHub - ampproject/worker-dom: The same DOM API and Frameworks you know, but in a Web Worker.
이 DOM 대단하다. 2018:worker-dom-mizchi's 블로그.
Worker-dom과 가장 큰 차이점은 Worker-dom이 DOM API를 모두 mock화한 후 해당 작업을 구현하고 전송한다는 것입니다.이 기초 위에서 얻은 작업은 비동기 API로 실현됩니다.const style = await window.getComputedStyle(element)partytown 비동기 숨기기const style = window.getComputedStyle(element)그게 왜 가능한지 알아봤어.

웹 워커에서main-thread 동기화 기술

  • main-thread에서worker와 서비스-worker를 시작합니다
  • worker에서 서비스-worker로 xhr(blocking 상태에서) 동기화 요청
  • 서비스-worker는onfetch 프로세서로 요청을 중단하고main-thread에postMessage
  • 를 보냅니다.
  • main-thread의 onmessage 처리 프로그램에서main-thread의 조작을 진행하고 그 결과를postMessage로 서비스워커
  • 로 되돌려줍니다
  • 서비스-worker의 onmessage를 통해 결과를 받고worker의 onfetch의response
  • 를 되돌려줍니다
    기본적으로 현대의 전단에서는 XHR 동기화를 금지하지만 웹 워커에서는 xhr 동기화가 차단돼도main-thread를 차단하지 않는다.그러나 이때 WebWorker의 벤트로베아가 멈추었기 때문에 Worker thread 측의 다른 병행 작업이 멈추었다.

    또한 클라이언트 객체에 대한 참조는 Proxy로 구성됩니다.Proxy 객체에 액세스할 때마다 Proxy의 반사는 XHR를 동기화하여 주 스레드 측면의 값을 해결합니다.

    partytown의 참신성


    Worker에서 XHR를 동기화할 생각이 없습니다.파티타운에만 한정된 것이 아니라 이 아이디어를 적용하면 다양한 일이 일어날 수 있으니 스스로 실현해 보자.

    해봤어요.


    나는 이 홈페이지에서preact 코드가 이동할 수 있는 곳을 만들었다.
    /** @jsx h */
    import { expose } from "comlink";
    import { stubDocumentOnWorker } from "./town";
    import { h, render } from "preact";
    import { useEffect, useState } from "preact/hooks";
    
    function App() {
      const [counter, setCounter] = useState(0);
      useEffect(() => {
        setInterval(() => {
          setCounter((n) => n + 1);
        }, 1000);
      }, []);
      return (
        <div>
          Hello,
          {counter}
        </div>
      );
    }
    
    const api = {
      start() {
        stubDocumentOnWorker();
        const el = document.createElement("div");
        render(<App />, el);
        document.body.appendChild(el);
      },
    };
    
    export type Api = typeof api;
    
    expose(api);
    

    이 설치 게으른 곳


    게으름을 많이 피워서 균형기를 설치하지 않았고 메인 라인 옆에 함수 처리 프로그램을 설치하지 않았기 때문에 onclick 등 이벤트 처리 프로그램을 처리할 수 없습니다.여기 힘 있으면 해.
    GitHub - immerjs/immer: Create the next immutable state by mutating the current one

    실장 해설


    소스 코드는 여기서 요점을 설명한다
    https://github.com/mizchi/mytown

    ServiceWorker


    우선 서비스 워크맨부터 실시합니다.onfetch에서 주 스레드에postMessage를 던지면 이 결과를 얻을 수 있습니다.
    town.js
    //...
    
    let cnt = 0;
    const _callbacks = new Map<number, (value: any) => void>();
    
    export function handleMessageOnServiceWorker(ev: MessageEvent) {
      const payload = ev.data as ResponsePayload;
      const fn = _callbacks.get(payload.id);
      fn?.(payload.value);
      _callbacks.delete(ev.data.id);
    }
    
    export async function handleFetchOnServiceWorker(event: any) {
      const url = new URL(event.request.url);
      const encodedCmd = url.search.substr(1);
      const cmd = JSON.parse(atob(encodedCmd)) as RemoteCommand;
      const id = cnt++;
      let resolve: any;
      const promise = new Promise<any>((r) => (resolve = r));
      _callbacks.set(id, resolve);
      // @ts-ignore
      const client = await clients.get(event.clientId);
      client.postMessage({
        type: "req",
        cmd,
        id,
      } as RequestPayload);
    
      const value = await promise;
      return new Response(JSON.stringify(value));
    }
    
    unique id를 생성하고 콜백의 맵에 Promise의 resolve 함수를 등록합니다.
    서비스워크맨의 온 메시지, 그 콜백을 해결하고 끝내십시오.
    이거 서비스워크맨 다람쥐한테 등록해./__town?... 요청자에게만handle FetchOn ServiceWorker에서 실행됩니다.
    src/sw.ts
    import {
      handleFetchOnServiceWorker,
      handleMessageOnServiceWorker,
    } from "./town";
    
    const log = (...args: any) => console.log("[sw]", ...args);
    
    self.addEventListener("install", (event: any) => {
      log("install", version);
      // @ts-ignore
      event.waitUntil(self.skipWaiting());
    });
    
    self.addEventListener("activate", (event: any) => {
      log("activate claim", version);
      // @ts-ignore
      event.waitUntil(self.clients.claim());
    });
    
    self.addEventListener("fetch", (event: any) => {
      const url = new URL(event.request.url);
      if (url.pathname.startsWith("/__town")) {
        log("handle", "/__town");
        return event.respondWith(handleFetchOnServiceWorker(event));
      }
    });
    
    self.addEventListener("message", handleMessageOnServiceWorker);
    
    바디의 인코딩이 번거롭기 때문에/__town?<base64> JSON의 베이스 64 인코딩을 받아들이기로 했습니다.

    worker 동기화 XHR


    그런 다음 worker에서 동기화 XHR로 끝점을 부르는 함수를 만듭니다.
    type Ptr = string | number;
    
    type RemoteValue =
      | {
          isPtr: true;
          ptr: Ptr;
          parentPtr?: Ptr;
        }
      | {
          isPtr: false;
          value: any;
        };
    
    export type RemoteCommand =
      | {
          op: "apply";
          ptr: Ptr;
          callerPtr?: Ptr;
          args: RemoteValue[];
        }
      | {
          op: "set";
          ptr: Ptr;
          key: string | number;
          value: RemoteValue;
        }
      | {
          op: "access";
          ptr: Ptr;
          key: string | number;
        };
    
    export const execCommandSyncOnWorker = (cmd: RemoteCommand): RemoteValue => {
      const encoded = btoa(JSON.stringify(cmd));
      const url = "/__town?" + encoded;
      let result: any;
      const xhr = new XMLHttpRequest();
      xhr.open("GET", url, false);
      xhr.onload = (_e) =>
        xhr.readyState === 4 && xhr.status === 200 && (result = xhr.responseText);
      xhr.onerror = console.error;
      xhr.send(null);
      return JSON.parse(result);
    };
    
    구성원에 대한 액세스, 대입, 함수 호출에 대한 RPC를 정의합니다.
    이때main-thread에서worker에게 JSON이 엄숙하게 할 수 없는 것을 보낼 수 없기 때문에 바늘 ID를 만들어 처리한다.실제로 이 ID는 Proxy로 패키지로 처리됩니다.

    main thread


    RPCServiceWorker <=> Main를 구현합니다.
    반환 값에 대한 객체를 생성할 때 해당 생성된 id를 RemoteValue로 반환합니다.이것은 instanceMap에 저장되어 id에서 실례를 찾을 수 있습니다.
    응용된 함수 호출
    // === main thread
    const instanceMap = new Map<Ptr, any>();
    if (globalThis.document) {
      instanceMap.set("document", document);
      instanceMap.set("window", window);
    }
    
    function isTransferrable(raw: any) {
      return raw == null || ["number", "string", "boolean"].includes(typeof raw);
    }
    
    const raw2remote = (raw: any): RemoteValue => {
      if (isTransferrable(raw)) {
        return {
          isPtr: false,
          value: raw,
        };
      } else {
        const newPtr = Math.random().toString(32).substr(2);
        instanceMap.set(newPtr, raw);
        return {
          isPtr: true,
          ptr: newPtr,
        };
      }
    };
    export function resolveCommandOnMain(command: RemoteCommand): RemoteValue {
      switch (command.op) {
        case "access": {
          const parent = instanceMap.get(command.ptr);
          const rawValue = parent[command.key];
          return raw2remote(rawValue);
        }
        case "set": {
          const parent = instanceMap.get(command.ptr);
          const rightValue = command.value.isPtr
            ? instanceMap.get(command.value.ptr)
            : command.value.value;
          parent[command.key] = rightValue;
          return raw2remote(undefined);
        }
        case "apply": {
          const fn = instanceMap.get(command.ptr);
          const rawArgs = command.args.map((value) => {
            return value.isPtr ? instanceMap.get(value.ptr) : value.value;
          });
          let rawValue;
          if (command.callerPtr) {
            const caller = instanceMap.get(command.callerPtr);
            rawValue = fn.apply(caller, rawArgs);
          } else {
            rawValue = fn(...rawArgs);
          }
          return raw2remote(rawValue);
        }
      }
    }
    
    을 주의하십시오. 함수를 직접 호출하여 참고하면this가 해결할 수 없으며 호출원의pstr를 모드document.querySelector(...)에 함께 전달할 수 있습니다.
    이것을postMessage의onmessage 프로세서로 건네주기
    // こういう型を定義してある
    export type RequestPayload = {
      type: "req";
      id: number;
      cmd: RemoteCommand;
    };
    
    export type ResponsePayload = {
      type: "res";
      id: number;
      value: any;
    };
    
    export function handleMessageOnMain(event: MessageEvent) {
      if (event.data.type === "req") {
        const req = event.data as RequestPayload;
        navigator.serviceWorker.controller!.postMessage({
          type: "res",
          id: req.id,
          value: resolveCommandOnMain(req.cmd),
        } as ResponsePayload);
      }
    }
    

    Worker의 Mock 객체


    Proxy를 사용하여 객체의 get/set/apply를 구현합니다.순서는 선착순이지만 Command RPC의 명칭은 여기서 결정된다.
    적용 오류를 방지하기 위해 적절한 함수를 기반으로 Proxy를 만듭니다.
    액세스 패턴에 따라 사전 준비
    export function createPtr(ptr: Ptr, parentPtr?: Ptr): any {
      return new Proxy(() => {}, {
        apply(_target, _thisArg, argumentsList) {
          const ret = execCommandSyncOnWorker({
            op: "apply",
            ptr: ptr,
            callerPtr: parentPtr,
            args: argumentsList.map((arg) => {
              if (isTransferrable(arg)) {
                return {
                  isPtr: false,
                  value: arg,
                };
              }
              return (
                arg._ptr ?? {
                  isPtr: false,
                  value: arg,
                }
              );
            }),
          } as RemoteCommand);
    
          if (ret.isPtr) {
            return createPtr(ret.ptr);
          } else {
            return ret.value;
          }
        },
        set(_target, propertyName, value, _receiver) {
          const remoteValue =
            typeof value === "object" && value._ptr
              ? value._ptr
              : {
                  isPtr: false,
                  value,
                };
          const _ret = execCommandSyncOnWorker({
            op: "set",
            ptr: ptr,
            key: propertyName,
            value: remoteValue,
          } as RemoteCommand);
          return true;
        },
        get(_target, propertyName) {
          if (propertyName == "_ptr") {
            return {
              ptr,
              isPtr: true,
            };
          }
          const ret = execCommandSyncOnWorker({
            op: "access",
            ptr: ptr,
            key: propertyName,
          } as RemoteCommand);
    
          if (ret.isPtr) {
            return createPtr(ret.ptr, ptr);
          } else {
            return ret.value;
          }
        },
      });
    }
    
    export const stubDocumentOnWorker = () => {
      globalThis.document = createPtr("document");
      globalThis.window = createPtr("window");
    };
    
    createPtr()는 귀속 구조로 생성된 대상은 구성원 방문에서 미리 준비한 동기화 XHR을 호출하고 createPtr()로 메인 라인에서 해결된 RemoteValue 패키지를 되돌려줍니다.
    워크맨 환경globalThis.documentdocument의 이름으로 이것을 등록합니다.(창문도)
    main-theard에 대한 설명은 없지만 main-theard의 instance Map에서 이 두 가지를 해결하기 위해 실례를 등록했습니다.
    // main thread
    instanceMap.set("document", document);
    instanceMap.set("window", window);
    
    이렇게 하면 창과 문서는 원격 대상으로 처리할 수 있다.

    워크맨부터 설치된 DOM 사용


    워크맨을 시작하는 곳 등 사랑을 끊지만 (vite+comlink에서 하고 있음) 워크맨에서 가짜document 대상을 조작하면 주 라인에서도 같은 동작이 적용됩니다.
    이 점을 증명하기 위해서, 나는preact를 가동해 보았다.
    /** @jsx h */
    
    import { expose } from "comlink";
    import { stubDocumentOnWorker } from "./town";
    import { h, render } from "preact";
    import { useEffect, useState } from "preact/hooks";
    
    function App() {
      const [counter, setCounter] = useState(0);
      useEffect(() => {
        setInterval(() => {
          setCounter((n) => n + 1);
        }, 1000);
      }, []);
      return (
        <div>
          Hello,
          {counter}
        </div>
      );
    }
    
    const api = {
      start() {
        stubDocumentOnWorker();
        const el = document.createElement("div");
        render(<App />, el);
        document.body.appendChild(el);
      },
    };
    
    export type Api = typeof api;
    
    expose(api);
    
    comlink를 통해 이 워커에 대해 start를 두드려 설치합니다.
    우선 서비스-worker를 등록합니다.
    왜냐하면
    // main-thread
    import Worker from "./worker?worker";
    import { wrap, Remote } from "comlink";
    import type { Api } from "./worker";
    import { handleMessageOnMain } from "./town";
    
    const api = wrap(new Worker()) as Remote<Api>;
    
    async function main() {
      const _reg = await navigator.serviceWorker.register("/sw.js");
      navigator.serviceWorker.addEventListener("message", handleMessageOnMain);
      const postButton = document.createElement("button");
      postButton.onclick = () => {
        api.start();
      };
      postButton.textContent = "start";
      document.body.appendChild(postButton);
    }
    
    main();
    
    이 동작이...

    끝맺다


    Proxy 대상의 기술은 스스로 실현하지만 가장자리 상황은 무한하기 때문에 상황에 집중하여 실현하는 것이 좋다.
    프록시 대상에 접근할 때마다 구성원들이 프로세스를 뛰어넘는 비동기적인 접근을 하기 때문에 성능이 좋지 않습니다.이렇게 생각하면 부작용을 분할할 수 있는 워크맨-dom과 같은 설치가 될 것 같다.
    겉으로 보기엔 동기생 같은 방법이 편리해 아침저녁으로 쓸 수 있다.

    좋은 웹페이지 즐겨찾기