tmp.spacet.me 데브로그 파트 2

19706 단어 dohackathon
이제 코어를 사용할 수 있으므로 외부 뷰어에서 파일을 여는 첫 번째 사용 사례를 다루려고 합니다.

이 작업을 단순화하기 위해 지금은 하드코딩된 외부 통합 목록을 사용하겠습니다. 이 목록을 사용자 지정 가능하게 만드는 것은 나중에 제공됩니다.

postMessage 프로토콜: 내가 사용하기로 결정한 메시징 프로토콜은 JSON-RPC 사양에 따라 Microsoft also uses it in its Language Server Protocol Specification 입니다.

결과



다음을 위한 통합을 구현했습니다.

  • json.spacet.me - JSON 뷰어

  • vdo.glitch.me - 비디오 플레이어



  • 작동 방식: 외부 뷰어로 파일을 열 때 tmp.spacet.me는 세션 ID를 전달하는 새 창을 엽니다. 뷰어가 세션 ID를 로드하고 감지하면 오프너에 JSON-RPC 호출을 만들어 파일 내용을 가져옵니다.



    구현 및 리팩토링



    doing the simplest thing that could possibly work으로 시작하여 postMessage 처리 코드를 하드코딩했습니다. 여기서 코드는 TypeScript 컴파일러를 만족시키기 위해 유형 주장으로 가득 차 있습니다. 실험할 때 저는 이것이 좋은 일이라고 생각합니다. 나중에 정리하는 것을 잊지 마세요. 바로 다음에 올 것입니다.

    여기에서 전체 코드를 이해할 필요는 없습니다. 주석 주변의 코드만 살펴보세요.

    window.addEventListener('message', async (e) => {
      const fromWindow = (e.source as unknown) as Window
    
      // Check for a method call.
      if (e.data.method === 'tmp/getOpenedFile') {
        // In this block, `e.data` is `any`, so no IntelliSense.
    
        // (hovertip) const sessionId: any
        const sessionId = e.data.params.sessionId
        const session = sessionStorage[`session:${sessionId}`]
        if (!session) return
        const sessionState = JSON.parse(session)
        const db = getFilesDatabase()
        const doc = await db.get(sessionState.openedFile, {
          binary: true,
          attachments: true,
        })
        fromWindow.postMessage(
          // Send a reply.
          {
            // Right now this object can be arbitrary payload;
            // there's currently no type-checking here.
            // So I might introduce a bug at some point...
            jsonrpc: '2.0',
            id: e.data.id,
            result: {
              blob: (doc._attachments.blob as any).data,
              file: {
                _rev: doc._rev,
                _id: doc._id,
                name: doc.name,
                type: doc.type,
              },
            },
          },
          e.origin
        )
      }
    })
    


    이제 이것이 작동하는 동안 이러한 코드 작성 방식이 곧 문제를 일으킬 수 있음을 알 수 있습니다. 지금 당장 이러한 질문에 답하려면 코드가 명시적으로 언급되거나 문서화되지 않았기 때문에 코드 구현 방법을 읽어야 합니다.
  • 어떤 방법을 사용할 수 있습니까?
  • 이러한 메서드는 어떤 종류의 매개변수를 사용합니까?
  • 각 방법에 대한 결과는 어떻게 됩니까?

  • 이로 인해 인터페이스가 깨지기 쉬우므로 원하지 않습니다. 이 경우 정적 타이핑이 여기서 도움이 될 수 있다고 생각합니다. 그래서 계속해서 RPC 인터페이스를 문서화하는 방법으로 TypeScript 인터페이스를 만들었습니다. I wrote it in a way that I want to read it in the future.

    interface RpcInterface extends JsonRpcDefinition {
      'tmp/getOpenedFile': {
        params: {
          sessionId: string
        }
        result: {
          blob: Blob
          file: {
            _rev: string
            _id: string
            name: string
            type: string
          }
        }
      }
    }
    


    이제 메시지 핸들러는 다음과 같습니다.

    const rpc = new JsonRpcPayloadChecker<RpcInterface>()
    
    window.addEventListener('message', async (e) => {
      const fromWindow = (e.source as unknown) as Window
      // Changed
      if (rpc.isMethodCall(e.data, 'tmp/getOpenedFile')) {
        // In this block, `e.data.params` shall have type
        // `RpcInterface['tmp/getOpenedFile']` which is `{ sessionId: string }`
    
        // (hovertip) const sessionId: string
        const sessionId = e.data.params.sessionId
        const session = sessionStorage[`session:${sessionId}`]
        if (!session) return
        const sessionState = JSON.parse(session)
        const db = getFilesDatabase()
        const doc = await db.get(sessionState.openedFile, {
          binary: true,
          attachments: true,
        })
        fromWindow.postMessage(
          // Changed
          rpc.replyResult(e.data, {
            // In here, the type should flow to this object.
            // So any deviations from the declared interface
            // would cause a type-checking error.
            blob: (doc._attachments.blob as any).data,
            file: {
              _rev: doc._rev,
              _id: doc._id,
              name: doc.name,
              type: doc.type,
            },
          }),
          e.origin
        )
      }
    })
    


    유형 흐름을 만들기 위해 다음은 해당 코드의 나머지 부분입니다. 이것은 내가 고급 유형을 작성하는 극소수의 장소 중 하나인 IMO입니다. IMO 고급 유형은 코드의 가독성을 떨어뜨리므로 격리된 장소에서 사용하는 것이 가장 좋습니다. TypeScript 세금은 문제지만 피하지 말고 관리합시다.

    고급 유형을 사용할 때 내가 사용하는 리트머스 테스트는 다음과 같습니다. 다른 파일에서 유형 주석의 양이 줄어듭니까? 고급 유형이 코드베이스의 나머지 부분을 감염시키지 마십시오!

    export class JsonRpcPayloadChecker<T extends JsonRpcDefinition> {
      isMethodCall<K extends keyof T>(
        message: unknown,
        method: K
      ): message is JsonRpcMethodCall<K, T[K]['params']> {
        return isJsonRpcMethodCall(message) && message.method === method
      }
    
      replyResult<K extends keyof T>(
        message: JsonRpcMethodCall<K, any>,
        result: T[K]['result']
      ) {
        return {
          jsonrpc: '2.0',
          id: message.id,
          result: result,
        }
      }
    }
    
    export interface JsonRpcDefinition {
      [methodName: string]: {
        params: any
        result?: any
      }
    }
    
    export interface JsonRpcMethodCall<MethodName, MethodParams> {
      id: string
      method: MethodName
      params: MethodParams
    }
    
    function isJsonRpcMethodCall(
      message: any
    ): message is { method: string; params: any } {
      return message && message.method
    }
    

    좋은 웹페이지 즐겨찾기