优化渲染器 (#250)

This commit is contained in:
戏舞天殇 2023-09-15 08:15:25 +08:00 committed by GitHub
parent 6cf294691c
commit e757537133
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 148 additions and 119 deletions

View File

@ -1,21 +1,17 @@
import Renderer from '../renderer/Renderer.js' import { segment } from 'oicq'
import Renderer from '../renderer/loader.js'
/** /**
* 暂时保留对手工引用puppeteer.js的兼容 * 暂时保留对手工引用puppeteer.js的兼容
* 后期会逐步废弃 * 后期会逐步废弃
* 只提供截图及分片截图功能 * 只提供截图及分片截图功能
*/ */
export default { let renderer = Renderer.getRenderer('puppeteer')
// 截图 renderer.screenshot = async (name, data) => {
async screenshot (name, data = {}) {
let renderer = Renderer.getRenderer()
let img = await renderer.render(name, data) let img = await renderer.render(name, data)
return img ? segment.image(img) : img return img ? segment.image(img) : img
}, }
renderer.screenshots = async (name, data) => {
// 分片截图
async screenshots (name, data = {}) {
let renderer = Renderer.getRenderer()
data.multiPage = true data.multiPage = true
let imgs = await renderer.render(name, data) || [] let imgs = await renderer.render(name, data) || []
let ret = [] let ret = []
@ -24,4 +20,5 @@ export default {
} }
return ret.length > 0 ? ret : false return ret.length > 0 ? ret : false
} }
}
export default renderer

View File

@ -1,39 +1,82 @@
import template from 'art-template'
import chokidar from 'chokidar'
import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import yaml from 'yaml'
import lodash from 'lodash'
import cfg from '../config/config.js'
import { Data } from '#miao'
let rendererBackends = {} export default class Renderer {
/**
* 渲染器
* @param data.id 渲染器ID
* @param data.type 渲染器类型
* @param data.render 渲染器入口
*/
constructor(data) {
/** 渲染器ID */
this.id = data.id || 'renderer'
/** 渲染器类型 */
this.type = data.type || 'image'
/** 渲染器入口 */
this.render = this[data.render || 'render']
this.dir = './temp/html'
this.html = {}
this.watcher = {}
this.createDir(this.dir)
}
/** 创建文件夹 */
createDir(dirname) {
if (fs.existsSync(dirname)) {
return true
} else {
if (this.createDir(path.dirname(dirname))) {
fs.mkdirSync(dirname)
return true
}
}
}
/** 模板 */
dealTpl(name, data) {
let { tplFile, saveId = name } = data
let savePath = `./temp/html/${name}/${saveId}.html`
/** 读取html模板 */
if (!this.html[tplFile]) {
this.createDir(`./temp/html/${name}`)
async function registerRendererBackends () {
const subFolders = fs.readdirSync(`${process.cwd()}/renderers`, { withFileTypes: true }).filter((dirent) => dirent.isDirectory())
for (let subFolder of subFolders) {
let name = subFolder.name
const rendererFn = await Data.importDefault(`/renderers/${name}/index.js`)
let configFile = `./renderers/${name}/config.yaml`
let rendererCfg = {}
if (fs.existsSync(configFile)) {
try { try {
rendererCfg = yaml.parse(fs.readFileSync(configFile, 'utf8')) this.html[tplFile] = fs.readFileSync(tplFile, 'utf8')
} catch (e) { } catch (error) {
rendererCfg = {} logger.error(`加载html错误${tplFile}`)
} return false
}
let renderer = rendererFn(rendererCfg)
if (!renderer.id || !renderer.type || !renderer.render || !lodash.isFunction(renderer.render)) {
logger.warn('渲染后端 ' + (renderer.id || subFolder.name) + ' 不可用')
}
rendererBackends[renderer.id] = renderer
logger.info(`加载渲染后端 ${renderer.id}`)
}
} }
await registerRendererBackends() this.watch(tplFile)
}
export default { data.resPath = `./resources/`
getRenderer () {
// TODO 渲染器降级 /** 替换模板 */
return rendererBackends[cfg.renderer?.name || 'puppeteer'] let tmpHtml = template.render(this.html[tplFile], data)
/** 保存模板 */
fs.writeFileSync(savePath, tmpHtml)
logger.debug(`[图片生成][使用模板] ${savePath}`)
return savePath
}
/** 监听配置文件 */
watch(tplFile) {
if (this.watcher[tplFile]) return
const watcher = chokidar.watch(tplFile)
watcher.on('change', path => {
delete this.html[tplFile]
logger.mark(`[修改html模板] ${tplFile}`)
})
this.watcher[tplFile] = watcher
} }
} }

56
lib/renderer/loader.js Normal file
View File

@ -0,0 +1,56 @@
import fs from 'node:fs'
import yaml from 'yaml'
import lodash from 'lodash'
import cfg from '../config/config.js'
import { Data } from '#miao'
import Renderer from './Renderer.js'
/** 全局变量 Renderer */
global.Renderer = Renderer
/**
* 加载渲染器
*/
class RendererLoader {
constructor() {
this.renderers = new Map()
this.dir = './renderers'
// TODO 渲染器热加载
this.watcher = {}
}
static async init() {
const render = new RendererLoader()
await render.load()
return render
}
async load() {
const subFolders = fs.readdirSync(this.dir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory())
for (let subFolder of subFolders) {
let name = subFolder.name
try {
const rendererFn = await Data.importDefault(`${this.dir}/${name}/index.js`)
let configFile = `${this.dir}/${name}/config.yaml`
let rendererCfg = fs.existsSync(configFile) ? yaml.parse(fs.readFileSync(configFile, 'utf8')) : {}
let renderer = rendererFn(rendererCfg)
if (!renderer.id || !renderer.type || !renderer.render || !lodash.isFunction(renderer.render)) {
logger.warn('渲染后端 ' + (renderer.id || subFolder.name) + ' 不可用')
}
this.renderers.set(renderer.id, renderer)
logger.info(`加载渲染后端 ${renderer.id}`)
} catch (err) {
logger.error(`渲染后端 ${name} 加载失败`)
logger.error(err)
}
}
}
getRenderer(name = cfg.renderer?.name || 'puppeteer') {
// TODO 渲染器降级
return this.renderers.get(name)
}
}
export default await RendererLoader.init()

View File

@ -10,13 +10,5 @@ import Puppeteer from './lib/puppeteer.js'
*/ */
export default function (config) { export default function (config) {
// TODO Puppeteer待简化重构 // TODO Puppeteer待简化重构
const PuppeteerRender = new Puppeteer(config) return new Puppeteer(config)
return {
id: 'puppeteer',
type: 'image',
async render (name, data) {
return await PuppeteerRender.screenshot(name, data)
}
}
} }

View File

@ -1,20 +1,22 @@
import fs from 'node:fs' import Renderer from '../../../lib/renderer/Renderer.js'
import os from 'node:os' import os from 'node:os'
import lodash from 'lodash' import lodash from 'lodash'
import template from 'art-template'
import chokidar from 'chokidar'
import puppeteer from 'puppeteer' import puppeteer from 'puppeteer'
// 暂时保留对原config的兼容 // 暂时保留对原config的兼容
import cfg from '../../../lib/config/config.js' import cfg from '../../../lib/config/config.js'
import { Data } from '#miao' import { Data } from '#miao'
import path from 'path'
const _path = process.cwd()
// mac地址 // mac地址
let mac = '' let mac = ''
export default class PuppeteerRenderer { export default class Puppeteer extends Renderer {
constructor(config) { constructor(config) {
super({
id: 'puppeteer',
type: 'image',
render: 'screenshot'
})
this.browser = false this.browser = false
this.lock = false this.lock = false
this.shoting = [] this.shoting = []
@ -39,22 +41,6 @@ export default class PuppeteerRenderer {
/** chromium其他路径 */ /** chromium其他路径 */
this.config.wsEndpoint = config.puppeteerWS || cfg?.bot?.puppeteer_ws this.config.wsEndpoint = config.puppeteerWS || cfg?.bot?.puppeteer_ws
} }
this.html = {}
this.watcher = {}
this.createDir('./temp/html')
}
createDir(dir) {
if (!fs.existsSync(dir)) {
let dirs = dir.split('/')
for (let idx = 1; idx <= dirs.length; idx++) {
let temp = dirs.slice(0, idx).join('/')
if (!fs.existsSync(temp)) {
fs.mkdirSync(temp)
}
}
}
} }
/** /**
@ -190,7 +176,7 @@ export default class PuppeteerRenderer {
try { try {
const page = await this.browser.newPage() const page = await this.browser.newPage()
let pageGotoParams = lodash.extend({ timeout: 120000 }, data.pageGotoParams || {}) let pageGotoParams = lodash.extend({ timeout: 120000 }, data.pageGotoParams || {})
await page.goto(`file://${_path}${lodash.trim(savePath, '.')}`, pageGotoParams) await page.goto('file://' + path.resolve(savePath), pageGotoParams)
let body = await page.$('#container') || await page.$('body') let body = await page.$('#container') || await page.$('body')
// 计算页面高度 // 计算页面高度
@ -282,51 +268,6 @@ export default class PuppeteerRenderer {
return data.multiPage ? ret : ret[0] return data.multiPage ? ret : ret[0]
} }
/** 模板 */
dealTpl(name, data) {
let { tplFile, saveId = name } = data
let savePath = `./temp/html/${name}/${saveId}.html`
/** 读取html模板 */
if (!this.html[tplFile]) {
this.createDir(`./temp/html/${name}`)
try {
this.html[tplFile] = fs.readFileSync(tplFile, 'utf8')
} catch (error) {
logger.error(`加载html错误${tplFile}`)
return false
}
this.watch(tplFile)
}
data.resPath = `${_path}/resources/`
/** 替换模板 */
let tmpHtml = template.render(this.html[tplFile], data)
/** 保存模板 */
fs.writeFileSync(savePath, tmpHtml)
logger.debug(`[图片生成][使用模板] ${savePath}`)
return savePath
}
/** 监听配置文件 */
watch(tplFile) {
if (this.watcher[tplFile]) return
const watcher = chokidar.watch(tplFile)
watcher.on('change', path => {
delete this.html[tplFile]
logger.mark(`[修改html模板] ${tplFile}`)
})
this.watcher[tplFile] = watcher
}
/** 重启 */ /** 重启 */
restart() { restart() {
/** 截图超过重启数时,自动关闭重启浏览器,避免生成速度越来越慢 */ /** 截图超过重启数时,自动关闭重启浏览器,避免生成速度越来越慢 */