/**
* @file Browser类
*/
const {remote} = require("electron")
const {Menu, MenuItem} = remote
const Mousetrap = require("mousetrap")
import EventEmitter from "./EventEmitter.js"
import {uniqueId, importStyle, setDomAsOffsetParent} from "./util.js"
import Page from "./Page.js"
import Target from "./Target.js"
import ChromeTabs from "./libs/chrome-tabs/chrome-tabs.js"
import ChromeTabsCSS from "./libs/chrome-tabs/chrome-tabs.css.js"
import {loadingGif, faviconPng} from "./images.js"
importStyle(ChromeTabsCSS)
/**
* @class Browser
* @extends EventEmitter
*
* @property {string} id browser实例的唯一id
* @property {boolean} isReady 是否为就绪状态,即构建完成,可以获取page
* @property {boolean} isFront 是否为激活状态
* @property {BrowserManager} browserManager
* @property {Object} options 传入的配置信息
*/
export default class Browser extends EventEmitter {
/**
* Browser构造函数
* @constructor Browser
*
* @param {BrowserManager} browserManager
* @param {Object} options 传入配置
* @param {Element} options.container DOM容器
* @param {number} options.autoGcTime 闲置后的自动回收时间,单位为ms, 为0时为永不回收,默认为0
* @param {number} options.autoGcLimit 打开的browser超过autoGcLimit时才开启自动回收, 默认20
* @param {number} options.pageLoadingTimeout 页面加载超时时间, 默认10s
* @param {boolean} options.createPage 是否新建默认page
* @param {boolean} [options.devtools] 是否打开控制台
* @param {string} [options.partition] session标识,相同的partition共享登录状态
* @param {string} options.preload preload, 理论上必须为当前包的preload/webivew.preload.js, 否则无法通信
* @param {string} [options.startUrl] 新建tab的初始页面, 不传则为abount:blank
* @param {string} [options.startUrlReferrer] 打开的startUrl的referrer
* @param {string} [options.webpreferences] 网页功能的设置
*/
constructor(browserManager, options) {
super()
this.isReady = false
this.isFront = false
this.id = uniqueId("browser_")
this._pages = []
this.browserManager = browserManager
this.options = options
}
/**
* Browser的初始化
* @async
*/
async init() {
this.build()
if (this.options.createPage) {
await this.newPage()
}
this.isReady = true
}
/**
* 获取browser相对于视窗的信息
*/
getBoundingClientRect() {
return this.doms.pagesContainer.getBoundingClientRect()
}
/**
* Browser的构建
*/
build() {
const template = `
<div class="electron-puppeteer-browser">
<div class="chrome-tabs">
<div class="chrome-tabs-content"></div>
<div class="chrome-tabs-content-add">
<svg viewBox="64 64 896 896" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z"></path><path d="M176 474h672q8 0 8 8v60q0 8-8 8H176q-8 0-8-8v-60q0-8 8-8z"></path></svg>
</div>
<div class="chrome-tabs-bottom-bar"></div>
</div>
<div class="electron-puppeteer-pages">
</div>
</div>
`
const div = document.createElement("div")
div.innerHTML = template
this.element = div.firstElementChild
setDomAsOffsetParent(this.options.container)
this.options.container.appendChild(this.element)
this.doms = {
tabsElm: this.element.querySelector(".chrome-tabs"),
addBtn: this.element.querySelector(".chrome-tabs-content-add"),
pagesContainer: this.element.querySelector(".electron-puppeteer-pages"),
}
this._initChromeTabs()
this._bindShortcuts()
}
/**
* 初始化顶部tab
* @private
*/
_initChromeTabs() {
let chromeTabs = (this._chromeTabs = new ChromeTabs())
chromeTabs.init(this.doms.tabsElm)
this.doms.tabsElm.addEventListener("activeTabChange", (evt) => {
if (!evt.detail.trigger) {
var pageId = evt.detail.tabEl.getAttribute("data-tab-id")
var page = this.getPageById(pageId)
page.bringToFront()
}
})
this.doms.tabsElm.addEventListener("tabRemove", (evt) => {
if (!evt.detail.trigger) {
var pageId = evt.detail.tabEl.getAttribute("data-tab-id")
var page = this.getPageById(pageId)
page.$tabEl = null
page.close()
}
})
this.doms.addBtn.addEventListener("click", () => {
this.newPage()
})
}
/**
* 绑定快捷键
* @private
*/
_bindShortcuts() {
this.shortcuts = [
{
action: "closePage",
prevent: true,
keys: ["command+w", "ctrl+w"],
callback: () => {
let frontPage = this.frontPage()
if (this.isFront && frontPage) {
frontPage.close()
}
},
},
{
action: "newPage",
prevent: true,
keys: ["command+t", "ctrl+t"],
callback: () => {
if (this.isFront) {
this.newPage()
}
},
},
{
action: "reload",
prevent: true,
keys: ["f5"],
callback: () => {
let frontPage = this.frontPage()
if (this.isFront && frontPage && frontPage.isReady) {
frontPage.reload()
}
},
},
{
action: "toggleDevtools",
prevent: true,
keys: ["command+option+i", "ctrl+shift+i", "f12"],
callback: () => {
let frontPage = this.frontPage()
if (this.isFront && frontPage && frontPage.isReady) {
if (frontPage.webview.isDevToolsOpened()) {
frontPage.webview.closeDevTools()
} else {
frontPage.webview.openDevTools()
}
}
},
},
]
this.shortcuts.forEach((item) => {
Mousetrap.bind(item.keys, () => {
if (item.callback) {
item.callback.call(this)
}
if (item.prevent) {
return false
}
})
})
}
_doBack() {
this._hideTimeStart = Date.now()
this.isFront = false
this.element.style.zIndex = -1
// 自动回收
if (this.options.autoGcTime) {
this._gcTimer = setTimeout(() => {
let autoGcLimit = this.options.autoGcLimit || 20
// 仅当打开的browser超过20个时才回收
if (this.browserManager.size > autoGcLimit) {
this.close()
}
}, this.options.autoGcTime)
}
/**
* 当前browser取消激活时触发
* @event Browser#back
*/
this.emit("back")
}
_doFront() {
if (this._gcTimer) {
clearTimeout(this._gcTimer)
}
this._hideTimeStart = 0
this.isFront = true
this.element.style.zIndex = 1
// 每次切换browser的时候强制重新布局,防止当前browser不可见时导致的tab样式错乱
this._chromeTabs.layoutTabs()
/**
* 当前browser激活时触发
* @event Browser#front
*/
this.emit("front")
}
/**
* 闲置时间
* 当前非激活时长,激活时会被清0
*
* @return {number} 单位为ms
*/
get idleTime() {
if (this.isFront || !this.isReady) {
return 0
}
return Date.now() - this._hideTimeStart
}
/**
* 将browser提到视窗最前端,相当于多个browser,切换到当前
*/
bringToFront() {
this.browserManager._bringBrowserToFront(this.id)
}
/**
* 关闭browser
*/
close() {
this._pages.forEach((page) => {
page.close()
})
this.options.container.removeChild(this.element)
this.browserManager._removeBrowser(this.id)
/**
* 当前browser关闭时触发
* @event Browser#close
*/
this.emit("close", this.id)
}
/**
* 获取browser下的所有page实例集合
* @async
*
* @return {Page[]}
*/
async pages() {
return this._pages.slice(0)
}
/**
* 通过pageid获取指定page实例
*
* @return {Page}
*/
getPageById(pageId) {
return this._pages.find((item) => item.id === pageId)
}
/**
* 获取当前激活的page
*
* @return {Page}
*/
frontPage() {
return this._pages.find((item) => item.isFront === true)
}
/**
* 新建页面
* @param {string} [url] 页面跳转地址,不传则跳转到browser的startUrl
* @param {string} [referrer] referrer,不传则为browser的startUrlReferrer
*
* @return {Promise<Page>} 返回构建的page实例
*/
newPage(url, referrer) {
let page = this._newPageWithoutReady(null, url, referrer)
return page._waitForReady().then(() => page)
}
/**
* 新建页面, 不等待页面加载完成
* @param {Target} opener 打开当前页面的opener
* @param {string} [url] 页面跳转地址,不传则跳转到browser的startUrl
* @param {string} [referrer] referrer,不传则为browser的startUrlReferrer
*
* @return {Page} 返回构建的page实例
*/
_newPageWithoutReady(opener, url, referrer) {
var page = new Page(this, {
container: this.doms.pagesContainer,
partition: this.options.partition,
devtools: this.options.devtools,
preload: this.options.preload,
loadingTimeout: this.options.pageLoadingTimeout,
startUrl: url || this.options.startUrl,
startUrlReferrer: referrer || this.options.startUrlReferrer,
})
/**
* 当前browser新建page时触发, 此时page还未构建完毕
* @event Browser#new-page
* @type {Page}
*/
this.emit("new-page", page)
let target = new Target(page, opener)
/**
* 打开新标签页时触发
* @event Browser#targetcreated
* @type {Target}
*/
this.emit("targetcreated", target)
page.on("connect", () => {
/**
* 打开的页面url变更时触发
* @event Browser#targetchanged
* @type {Target}
*/
this.emit("targetchanged", target)
})
page.once("close", () => {
/**
* 新打开的页面关闭时触发
* @event Browser#targetdestroyed
* @type {Target}
*/
this.emit("targetdestroyed", target)
})
page._injectShortcuts(this.shortcuts.slice(0))
page.init()
this._pages.push(page)
this._handlePageTab(page)
page.bringToFront()
return page
}
/**
* page对应tab的右键菜单,图标及标题的更新
* @private
* @param {Page} page
*/
_handlePageTab(page) {
let elm = (page.$tabEl = this._chromeTabs.addTab(
{
id: page.id,
title: "加载中……",
favicon: faviconPng,
},
{
background: true,
}
))
elm.addEventListener("contextmenu", () => {
this._showTabMenu(page)
})
let iconSet = false
page.on("loading-start", () => {
iconSet = false
this._chromeTabs.updateTab(page.$tabEl, {
favicon: loadingGif,
})
})
page.on("favicon-updated", (evt) => {
iconSet = true
this._chromeTabs.updateTab(page.$tabEl, {
favicon: evt.favicon,
})
var img = new Image()
img.src = evt.favicon
img.onerror = () => {
this._chromeTabs.updateTab(page.$tabEl, {
favicon: faviconPng,
})
img.onerror = null
img = null
}
})
page.on("title-updated", (evt) => {
this._chromeTabs.updateTab(page.$tabEl, {
title: evt.title,
})
})
page.on("loading-end", () => {
if (!iconSet) {
this._chromeTabs.updateTab(page.$tabEl, {
favicon: faviconPng,
})
}
})
page.on("new-window", async (evt) => {
let url = page.url()
let newPage = this._newPageWithoutReady(page.target(), evt.url, url)
try {
evt.returnValue = newPage.webview.getWebContents().id
} catch (e) {}
})
}
/**
* tab的右键菜单
* @private
* @param {Page} page
*/
_showTabMenu(page) {
//右键菜单
const menu = new Menu()
menu.append(
new MenuItem({
label: "刷新 F5",
click: () => {
page.reload()
},
})
)
menu.append(
new MenuItem({
label: "复制",
click: () => {
this.newPage(page.url(), page.url())
},
})
)
menu.append(
new MenuItem({
type: "separator",
})
)
menu.append(
new MenuItem({
label: "前进",
enabled: page.canGoForward(),
click: () => {
page.goForward()
},
})
)
menu.append(
new MenuItem({
label: "后退",
enabled: page.canGoBack(),
click: () => {
page.goBack()
},
})
)
menu.append(
new MenuItem({
type: "separator",
})
)
menu.append(
new MenuItem({
label: "关闭 Ctrl+W",
click: () => {
page.close()
},
})
)
menu.append(
new MenuItem({
label: "关闭其它",
click: () => {
this._pages
.filter((item) => item.id !== page.id)
.forEach((item) => item.close())
},
})
)
menu.popup({window: remote.getCurrentWindow()})
}
/**
* 删除页面,不可直接调用
* 如需要关闭页面,请调用page.close()
* @private
* @param {string} pageId
*/
_removePage(pageId) {
var index = this._pages.findIndex((page) => page.id === pageId)
var page = this._pages[index]
this._pages.splice(index, 1)
if (page.$tabEl) {
this._chromeTabs.removeTab(page.$tabEl, true)
}
if (!page.isFront) {
return
}
var nextPage = this._pages[index] || this._pages[index - 1]
if (nextPage) {
nextPage.bringToFront()
}
}
/**
* 激活页面,不可直接调用
* 如需要激活页面,请调用page.bringToFront()
* @private
* @param {string} pageId
*/
_bringPageToFront(pageId) {
this._pages.forEach((page) => {
if (pageId === page.id) {
this._chromeTabs.setCurrentTab(page.$tabEl, true)
page._doFront()
} else {
page._doBack()
}
})
}
}