浏览代码

improve(plugin): simplify caller

charlie 4 年之前
父节点
当前提交
94c3857da4
共有 6 个文件被更改,包括 779 次插入369 次删除
  1. 0 2
      libs/package.json
  2. 22 17
      libs/src/LSPlugin.caller.ts
  3. 423 0
      libs/src/postmate/index.ts
  4. 2 2
      libs/tsconfig.json
  5. 331 347
      libs/yarn.lock
  6. 1 1
      package.json

+ 0 - 2
libs/package.json

@@ -18,14 +18,12 @@
     "dompurify": "^2.2.7",
     "eventemitter3": "^4.0.7",
     "path": "^0.12.7",
-    "postmate": "^1.5.2",
     "snake-case": "^3.0.4"
   },
   "devDependencies": {
     "@types/debug": "^4.1.5",
     "@types/dompurify": "^2.2.1",
     "@types/lodash-es": "^4.17.4",
-    "@types/postmate": "^1.5.1",
     "ts-loader": "^8.0.17",
     "typescript": "^4.2.2",
     "webpack": "^5.24.3",

+ 22 - 17
libs/src/LSPlugin.caller.ts

@@ -1,8 +1,8 @@
-import Postmate from 'postmate'
+import Debug from 'debug'
+import { Postmate, Model, ParentAPI, ChildAPI } from './postmate'
 import EventEmitter from 'eventemitter3'
 import { PluginLocal } from './LSPlugin.core'
-import Debug from 'debug'
-import { deferred } from './helpers'
+import { deferred, IS_DEV } from './helpers'
 import { LSPluginShadowFrame } from './LSPlugin.shadow'
 
 const debug = Debug('LSPlugin:caller')
@@ -25,8 +25,8 @@ export const AWAIT_LSPMSGFn = (id: string) => `${FLAG_AWAIT}${id}`
 class LSPluginCaller extends EventEmitter {
   private _connected: boolean = false
 
-  private _parent?: Postmate.ParentAPI
-  private _child?: Postmate.ChildAPI
+  private _parent?: ParentAPI
+  private _child?: ChildAPI
 
   private _shadow?: LSPluginShadowFrame
 
@@ -87,7 +87,7 @@ class LSPluginCaller extends EventEmitter {
       },
 
       [LSPMSG]: async ({ ns, type, payload }: any) => {
-        debug(`${this._debugTag} [call from host]`, ns, type, payload)
+        debug(`[call from host] ${this._debugTag}`, ns, type, payload)
 
         if (ns && ns.startsWith('hook')) {
           caller.emit(`${ns}:${type}`, payload)
@@ -98,7 +98,7 @@ class LSPluginCaller extends EventEmitter {
       },
 
       [LSPMSG_SYNC]: ({ _sync, result }: any) => {
-        debug(`sync reply #${_sync}`, result)
+        debug(`[sync reply] #${_sync}`, result)
 
         if (syncActors.has(_sync)) {
           const actor = syncActors.get(_sync)
@@ -123,11 +123,12 @@ class LSPluginCaller extends EventEmitter {
       return JSON.parse(JSON.stringify(this._pluginLocal?.toJSON()))
     }
 
-    const handshake = new Postmate.Model(model)
+    const pm = new Model(model)
+    const handshake = pm.sendHandshakeReply()
 
     this._status = 'pending'
 
-    await handshake.then(refParent => {
+    await handshake.then((refParent: ChildAPI) => {
       this._child = refParent
       this._connected = true
 
@@ -150,7 +151,7 @@ class LSPluginCaller extends EventEmitter {
         try {
           model[type](payload)
         } catch (e) {
-          debug(`model method #${type} not existed`)
+          debug(`[model method] #${type} not existed`)
         }
       }
 
@@ -187,15 +188,20 @@ class LSPluginCaller extends EventEmitter {
   }
 
   async _setupIframeSandbox () {
+    const cnt = document.body
     const pl = this._pluginLocal!
+    const url = new URL(pl.options.entry!)
+
+    url.searchParams
+      .set(`__v__`, IS_DEV ? Date.now().toString() : pl.options.version)
 
-    const handshake = new Postmate({
-      container: document.body,
-      url: pl.options.entry!,
+    const pt = new Postmate({
+      container: cnt, url: url.href,
       classListArray: ['lsp-iframe-sandbox'],
       model: { baseInfo: JSON.parse(JSON.stringify(pl.toJSON())) }
     })
 
+    let handshake = pt.sendHandshake()
     this._status = 'pending'
 
     // timeout for handshake
@@ -206,7 +212,7 @@ class LSPluginCaller extends EventEmitter {
         reject(new Error(`handshake Timeout`))
       }, 3 * 1000) // 3secs
 
-      handshake.then(refChild => {
+      handshake.then((refChild: ParentAPI) => {
         this._parent = refChild
         this._connected = true
         this.emit('connected')
@@ -243,7 +249,7 @@ class LSPluginCaller extends EventEmitter {
         clearTimeout(timer)
       })
     }).catch(e => {
-      debug('iframe sandbox error', e)
+      debug('[iframe sandbox] error', e)
       throw e
     }).finally(() => {
       this._status = undefined
@@ -265,7 +271,6 @@ class LSPluginCaller extends EventEmitter {
       this._call = async (type, payload = {}, actor) => {
         actor && (payload.actor = actor)
 
-        // TODO: support sync call
         // @ts-ignore Call in same thread
         this._pluginLocal?.emit(type, Object.assign(payload, {
           $$pid: pl.id
@@ -289,7 +294,7 @@ class LSPluginCaller extends EventEmitter {
         }
       }
     } catch (e) {
-      debug('shadow sandbox error', e)
+      debug('[shadow sandbox] error', e)
       throw e
     } finally {
       this._status = undefined

+ 423 - 0
libs/src/postmate/index.ts

@@ -0,0 +1,423 @@
+// Fork from https://github.com/dollarshaveclub/postmate
+
+/**
+ * The type of messages our frames our sending
+ * @type {String}
+ */
+export const messageType = 'application/x-postmate-v1+json'
+
+/**
+ * The maximum number of attempts to send a handshake request to the parent
+ * @type {Number}
+ */
+export const maxHandshakeRequests = 5
+
+/**
+ * A unique message ID that is used to ensure responses are sent to the correct requests
+ * @type {Number}
+ */
+let _messageId = 0
+
+/**
+ * Increments and returns a message ID
+ * @return {Number} A unique ID for a message
+ */
+export const generateNewMessageId = () => ++_messageId
+
+/**
+ * Postmate logging function that enables/disables via config
+ */
+export const log = (...args) => Postmate.debug ? console.log(...args) : null
+
+/**
+ * Takes a URL and returns the origin
+ * @param  {String} url The full URL being requested
+ * @return {String}     The URLs origin
+ */
+export const resolveOrigin = (url) => {
+  const a = document.createElement('a')
+  a.href = url
+  const protocol = a.protocol.length > 4 ? a.protocol : window.location.protocol
+  const host = a.host.length ? ((a.port === '80' || a.port === '443') ? a.hostname : a.host) : window.location.host
+  return a.origin || `${protocol}//${host}`
+}
+
+const messageTypes = {
+  handshake: 1,
+  'handshake-reply': 1,
+  call: 1,
+  emit: 1,
+  reply: 1,
+  request: 1,
+}
+
+/**
+ * Ensures that a message is safe to interpret
+ * @param  {Object} message The postmate message being sent
+ * @param  {String|Boolean} allowedOrigin The whitelisted origin or false to skip origin check
+ * @return {Boolean}
+ */
+export const sanitize = (message, allowedOrigin) => {
+  if (
+    typeof allowedOrigin === 'string' &&
+    message.origin !== allowedOrigin
+  ) return false
+  if (!message.data) return false
+  if (
+    typeof message.data === 'object' &&
+    !('postmate' in message.data)
+  ) return false
+  if (message.data.type !== messageType) return false
+  if (!messageTypes[message.data.postmate]) return false
+  return true
+}
+
+/**
+ * Takes a model, and searches for a value by the property
+ * @param  {Object} model     The dictionary to search against
+ * @param  {String} property  A path within a dictionary (i.e. 'window.location.href')
+ *                            passed to functions in the child model
+ * @return {Promise}
+ */
+export const resolveValue = (model, property) => {
+  const unwrappedContext = typeof model[property] === 'function'
+    ? model[property]() : model[property]
+  return Promise.resolve(unwrappedContext)
+}
+
+/**
+ * Composes an API to be used by the parent
+ * @param {Object} info Information on the consumer
+ */
+export class ParentAPI {
+  public parent: Window
+  public frame: HTMLIFrameElement
+  public child: Window
+  public events = {}
+  public childOrigin: string
+  public listener: (e: any) => void
+
+  constructor (info: Postmate) {
+    this.parent = info.parent
+    this.frame = info.frame
+    this.child = info.child
+    this.childOrigin = info.childOrigin
+
+    if (process.env.NODE_ENV !== 'production') {
+      log('Parent: Registering API')
+      log('Parent: Awaiting messages...')
+    }
+
+    this.listener = (e) => {
+      if (!sanitize(e, this.childOrigin)) return false
+
+      /**
+       * the assignments below ensures that e, data, and value are all defined
+       */
+      const { data, name } = (((e || {}).data || {}).value || {})
+
+      if (e.data.postmate === 'emit') {
+        if (process.env.NODE_ENV !== 'production') {
+          log(`Parent: Received event emission: ${name}`)
+        }
+        if (name in this.events) {
+          this.events[name].forEach(callback => {
+            callback.call(this, data)
+          })
+        }
+      }
+    }
+
+    this.parent.addEventListener('message', this.listener, false)
+    if (process.env.NODE_ENV !== 'production') {
+      log('Parent: Awaiting event emissions from Child')
+    }
+  }
+
+  get (property) {
+    return new Promise((resolve) => {
+      // Extract data from response and kill listeners
+      const uid = generateNewMessageId()
+      const transact = (e) => {
+        if (e.data.uid === uid && e.data.postmate === 'reply') {
+          this.parent.removeEventListener('message', transact, false)
+          resolve(e.data.value)
+        }
+      }
+
+      // Prepare for response from Child...
+      this.parent.addEventListener('message', transact, false)
+
+      // Then ask child for information
+      this.child.postMessage({
+        postmate: 'request',
+        type: messageType,
+        property,
+        uid,
+      }, this.childOrigin)
+    })
+  }
+
+  call (property, data) {
+    // Send information to the child
+    this.child.postMessage({
+      postmate: 'call',
+      type: messageType,
+      property,
+      data,
+    }, this.childOrigin)
+  }
+
+  on (eventName, callback) {
+    if (!this.events[eventName]) {
+      this.events[eventName] = []
+    }
+    this.events[eventName].push(callback)
+  }
+
+  destroy () {
+    if (process.env.NODE_ENV !== 'production') {
+      log('Parent: Destroying Postmate instance')
+    }
+    window.removeEventListener('message', this.listener, false)
+    this.frame.parentNode.removeChild(this.frame)
+  }
+}
+
+/**
+ * Composes an API to be used by the child
+ * @param {Object} info Information on the consumer
+ */
+export class ChildAPI {
+  private model: any
+  private parent: Window
+  private parentOrigin: string
+  private child: Window
+
+  constructor (info: Model) {
+    this.model = info.model
+    this.parent = info.parent
+    this.parentOrigin = info.parentOrigin
+    this.child = info.child
+
+    if (process.env.NODE_ENV !== 'production') {
+      log('Child: Registering API')
+      log('Child: Awaiting messages...')
+    }
+
+    this.child.addEventListener('message', (e) => {
+      if (!sanitize(e, this.parentOrigin)) return
+
+      if (process.env.NODE_ENV !== 'production') {
+        log('Child: Received request', e.data)
+      }
+
+      const { property, uid, data } = e.data
+
+      if (e.data.postmate === 'call') {
+        if (property in this.model && typeof this.model[property] === 'function') {
+          this.model[property](data)
+        }
+        return
+      }
+
+      // Reply to Parent
+      resolveValue(this.model, property)
+        .then(value => {
+          // @ts-ignore
+          e.source.postMessage({
+            property,
+            postmate: 'reply',
+            type: messageType,
+            uid,
+            value,
+          }, e.origin)
+        })
+    })
+  }
+
+  emit (name, data) {
+    if (process.env.NODE_ENV !== 'production') {
+      log(`Child: Emitting Event "${name}"`, data)
+    }
+    this.parent.postMessage({
+      postmate: 'emit',
+      type: messageType,
+      value: {
+        name,
+        data,
+      },
+    }, this.parentOrigin)
+  }
+}
+
+export type PostMateOptions = {
+  container: HTMLElement
+  url: string
+  classListArray?: Array<string>
+  name?: string
+  model?: any
+}
+
+/**
+ * The entry point of the Parent.
+ */
+export class Postmate {
+  static debug = false // eslint-disable-line no-undef
+  public container?: HTMLElement
+  public parent: Window
+  public frame: HTMLIFrameElement
+  public child?: Window
+  public childOrigin?: string
+  public url: string
+  public model: any
+  static Model: any
+
+  /**
+   * @param opts
+   */
+  constructor (opts: PostMateOptions) {
+    this.container = opts.container
+    this.url = opts.url
+    this.parent = window
+    this.frame = document.createElement('iframe')
+    this.frame.name = opts.name || ''
+    this.frame.classList.add.apply(this.frame.classList, opts.classListArray || [])
+    this.container.appendChild(this.frame)
+    this.child = this.frame.contentWindow
+    this.model = opts.model || {}
+  }
+
+  /**
+   * Begins the handshake strategy
+   * @param  {String} url The URL to send a handshake request to
+   * @return {Promise}     Promise that resolves when the handshake is complete
+   */
+  sendHandshake (url?: string) {
+    url = url || this.url
+    const childOrigin = resolveOrigin(url)
+    let attempt = 0
+    let responseInterval
+    return new Promise((resolve, reject) => {
+      const reply = (e: any) => {
+        if (!sanitize(e, childOrigin)) return false
+        if (e.data.postmate === 'handshake-reply') {
+          clearInterval(responseInterval)
+          if (process.env.NODE_ENV !== 'production') {
+            log('Parent: Received handshake reply from Child')
+          }
+          this.parent.removeEventListener('message', reply, false)
+          this.childOrigin = e.origin
+          if (process.env.NODE_ENV !== 'production') {
+            log('Parent: Saving Child origin', this.childOrigin)
+          }
+          return resolve(new ParentAPI(this))
+        }
+
+        // Might need to remove since parent might be receiving different messages
+        // from different hosts
+        if (process.env.NODE_ENV !== 'production') {
+          log('Parent: Invalid handshake reply')
+        }
+        return reject('Failed handshake')
+      }
+
+      this.parent.addEventListener('message', reply, false)
+
+      const doSend = () => {
+        attempt++
+        if (process.env.NODE_ENV !== 'production') {
+          log(`Parent: Sending handshake attempt ${attempt}`, { childOrigin })
+        }
+        this.child.postMessage({
+          postmate: 'handshake',
+          type: messageType,
+          model: this.model,
+        }, childOrigin)
+
+        if (attempt === maxHandshakeRequests) {
+          clearInterval(responseInterval)
+        }
+      }
+
+      const loaded = () => {
+        doSend()
+        responseInterval = setInterval(doSend, 500)
+      }
+
+      this.frame.addEventListener('load', loaded)
+
+      if (process.env.NODE_ENV !== 'production') {
+        log('Parent: Loading frame', { url })
+      }
+      this.frame.src = url
+    })
+  }
+}
+
+/**
+ * The entry point of the Child
+ */
+export class Model {
+  public child: Window
+  public model: any
+  public parent: Window
+  public parentOrigin: string
+
+  /**
+   * Initializes the child, model, parent, and responds to the Parents handshake
+   * @param {Object} model Hash of values, functions, or promises
+   * @return {Promise}       The Promise that resolves when the handshake has been received
+   */
+  constructor (model) {
+    this.child = window
+    this.model = model
+    this.parent = this.child.parent
+  }
+
+  /**
+   * Responds to a handshake initiated by the Parent
+   * @return {Promise} Resolves an object that exposes an API for the Child
+   */
+  sendHandshakeReply () {
+    return new Promise((resolve, reject) => {
+      const shake = (e) => {
+        if (!e.data.postmate) {
+          return
+        }
+        if (e.data.postmate === 'handshake') {
+          if (process.env.NODE_ENV !== 'production') {
+            log('Child: Received handshake from Parent')
+          }
+          this.child.removeEventListener('message', shake, false)
+          if (process.env.NODE_ENV !== 'production') {
+            log('Child: Sending handshake reply to Parent')
+          }
+          e.source.postMessage({
+            postmate: 'handshake-reply',
+            type: messageType,
+          }, e.origin)
+          this.parentOrigin = e.origin
+
+          // Extend model with the one provided by the parent
+          const defaults = e.data.model
+          if (defaults) {
+            Object.keys(defaults).forEach(key => {
+              this.model[key] = defaults[key]
+            })
+            if (process.env.NODE_ENV !== 'production') {
+              log('Child: Inherited and extended model from Parent')
+            }
+          }
+
+          if (process.env.NODE_ENV !== 'production') {
+            log('Child: Saving Parent origin', this.parentOrigin)
+          }
+          return resolve(new ChildAPI(this))
+        }
+        return reject('Handshake Reply Failed')
+      }
+      this.child.addEventListener('message', shake, false)
+    })
+  }
+}

+ 2 - 2
libs/tsconfig.json

@@ -37,11 +37,11 @@
     // "strict": true,
     /* Enable all strict type-checking options. */
     // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
-    "strictNullChecks": true,
+    // "strictNullChecks": true,
     /* Enable strict null checks. */
     // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
     // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
-    "strictPropertyInitialization": true,
+    // "strictPropertyInitialization": true,
     /* Enable strict checking of property initialization in classes. */
     // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
     // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

文件差异内容过多而无法显示
+ 331 - 347
libs/yarn.lock


+ 1 - 1
package.json

@@ -45,7 +45,7 @@
         "css:build": "postcss tailwind.all.css -o static/css/style.css --verbose --env production",
         "css:watch": "TAILWIND_MODE=watch postcss tailwind.all.css -o static/css/style.css --verbose --watch",
         "cljs:watch": "clojure -M:cljs watch parser-worker app electron",
-        "cljs:electron-watch": "PWD=$(pwd) PATCH_PARSER_WORKER=$(cat \"./templates/patch.parser.woker.js\" | sed -e \"s#PWD_ROOT#${PWD}#g\") clojure -M:cljs watch parser-worker app electron",
+        "cljs:electron-watch": "PATCH_PARSER_WORKER=$(cat \"./templates/patch.parser.woker.js\" | sed -e \"s#PWD_ROOT#`pwd`#g\") clojure -M:cljs watch parser-worker app electron",
         "cljs:release": "clojure -M:cljs release parser-worker app publishing electron",
         "cljs:test": "clojure -M:test compile test",
         "cljs:run-test": "node static/tests.js",

部分文件因为文件数量过多而无法显示