File size: 5,343 Bytes
6d0ad0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import crypto from 'crypto'
import http from './express.js'
import { Config, puppeteer, logger } from '#karin'
import VueCache from './VueFileCache.js'

const port = Config.Config.http.port

export default class Server {
  constructor (socket, request) {
    this.id = 0
    this.socket = socket
    this.request = request
    this.host = this.getHost()
    this.vueCache = VueCache
  }

  init () {
    this.socket.on('message', data => this.message(data))
    this.socket.on('close', () => {
      logger.error(`[Server][连接关闭]:${this.request.url}`)
    })
  }

  getHost () {
    const isSecure = this.request.connection.encrypted
    const protocol = isSecure ? 'wss' : 'ws'
    const host = this.request.headers.host
    const path = this.request.url
    return `${protocol}://${host}${path}`
  }

  /**

   * 事件处理

   * @param {puppeteer} data - 请求参数

   * @param {string} data.echo - 唯一标识符

   * @param {'heartbeat'|'render'|'static'} data.action - 请求类型

   * @param {object} data.data - 请求参数

   * @param {string} data.data.file - 请求文件

   * @param {'png'|'jpeg'|'webp'} [data.data.type] - 图片类型

   * @param {number} [data.data.quality] - 图片质量

   * @param {boolean} [data.data.omitBackground] - 是否忽略背景

   * @param {object} [data.data.setViewport] - 设置视口

   * @param {number} data.data.setViewport.width - 视口宽度

   * @param {number} data.data.setViewport.height - 视口高度

   * @param {number} [data.data.setViewport.deviceScaleFactor] - 设备比例

   * @param {boolean|number} data.data.multiPage - 是否多页 如果传递数字则视为视窗高度

   * @param {'load'|'domcontentloaded'|'networkidle0'|'networkidle2'} [data.data.waitUntil] - 等待时间

   * @param {any} data.data.pageGotoParams - 新建页面时传递给page.goto的参数

   */
  async message (data) {
    data = JSON.parse(data)
    switch (data.action) {
      case 'heartbeat':
        return logger.debug(`[Server][心跳]:${this.request.url}`)
      case 'render': {
        /** 这里接收到的是file://或者http地址 */
        data.data.file = decodeURIComponent(data.data.file)
        logger.debug(`[正向WS][渲染] URL: ${this.host} html:${data.data.file}`)
        let res
        if (data.data.vue && Config.Config.server.http) {
          const cacheId = this.vueCache.addCache(data.data.file, data.data.name, data.data.props)
          data.data.file = `http://localhost:${Config.Config.http.port}/vue/${data.data.vueTemplate || 'default'}/?id=${cacheId}`
          res = await puppeteer.screenshot(data.data)
          this.vueCache.deleteCache(cacheId)
        } else {
          res = await puppeteer.screenshot(data.data)
        }
        res.echo = data.echo
        res.action = 'renderRes'
        this.socket.send(JSON.stringify(res))
        break
      }
      case 'renderHtml': {
        /** 这里接收到的是完整html buffer */
        const file = decodeURIComponent(data.data.file)
        logger.debug(`[正向WS][渲染] URL: ${this.host} html:${file}`)
        /** 构建唯一id */
        const hash = `render-${this.id}-${crypto.randomUUID()}`
        const host = `http://localhost:${port}/api/render?hash=${hash}`
        http.file.set(hash, { ws: this, file })
        /** 替换为本地http */
        data.data.file = host
        /** http响应头 */
        data.data.hash = hash
        const res = await puppeteer.screenshot(data.data)
        res.echo = data.echo
        res.action = 'renderRes'
        this.socket.send(JSON.stringify(res))
        break
      }
      case 'static': {
        /** 这里接收的全是静态资源 由pupp主动请求客户端 客户端上报完成 */
        this.socket.emit(data.echo, data)
        break
      }
      default:
        break
    }
  }

  /**

   * 发送数据

   * @param {{

   *  type: string,

   *  action: 'heartbeat'|'static',

   *  data: {

   *    file: string

   * }

   * }} params 请求参数

   * @param {number} time 超时时间

   * @returns {Promise<void>}

   */
  async SendApi (action, params, time = 120) {
    const echo = crypto.randomUUID()
    const request = JSON.stringify({ echo, action, params })
    return new Promise((resolve, reject) => {
      this.socket.send(request)
      this.socket.once(echo, (data) => {
        if (data.status === 'ok') {
          resolve(data.data)
        } else {
          logger.error(`[Api请求错误] ${JSON.stringify(data, null, 2)}`)
          reject(data)
        }
      })
      /** 设置一个超时计时器 */
      setTimeout(() => {
        reject(new Error('API请求超时'))
      }, time * 1000)
    })
  }
}

/**

 * @typedef {{

 * echo: string,

 * action: 'heartbeat'|'render'|'static',

 * data: data

 * }} puppeteer

 */

/**

 * @typedef {{

 *  file: string,

 *  name: string,

 *  type?: 'png'|'jpeg'|'webp',

 *  quality?: number,

 *  omitBackground?: boolean,

 *  setViewport?: {

 *   width: number,

 *   height: number,

 *   deviceScaleFactor?: number,

 * },

 *  multiPage: boolean|number,

 *  waitUntil?: 'load'|'domcontentloaded'|'networkidle0'|'networkidle2',

 *  pageGotoParams: any

 * }} data

 */