/**
* @file Page Page类
*/
import EventEmitter from "./EventEmitter.js"
import Frame from "./Frame.js"
import {
uniqueId,
proxyBindDecorator,
importStyle,
TimeoutPromise,
} from "./util.js"
import {BoundIpc} from "./ipc.js"
import styleCss from "./style.css.js"
importStyle(styleCss)
const {remote} = require("electron")
const contextMenu = require("electron-context-menu")
/**
* @class Page
* @extends EventEmitter
*
* @property {string} id 当前实例的唯一id
* @property {boolean} closed 当前page是否关闭
* @property {boolean} isFront 当前page是否激活状态
* @property {boolean} isReady 首次dom-ready后isReady为true
* @property {Element} container 容器dom
* @property {WebviewTag} webview 页面对应的webview
* @property {Session} session webview对应的Session实例
* @property {WebRequest} webRequest webview对应的WebRequest实例
* @property {Ipc} ipc ipc通信实例,可用于和webview的主页面和所有iframe通信
* @property {Array} shortcuts 快捷键配置,由Browser注入
* @property {Object} options 传入的配置信息
*
*/
class Page extends EventEmitter {
/**
* Page构造函数
* @constructor Page
*
* @param {Browser} browser
* @param {PageOptions} options 传入配置
* @param {Element} options.container DOM容器
* @param {boolean} [options.devtools] 是否打开控制台
* @param {string} [options.partition] session标识,相同的partition共享登录状态
* @param {number} [options.loadingTimeout] 页面加载超时时间, 默认10s
* @param {string} [options.startUrl] 初始页面
* @param {string} [options.startUrlReferrer] startUrl的referrer
* @param {string} options.preload preload的脚本路径, 理论上必须为当前包的preload/webivew.preload.js
* @param {string} [options.webpreferences] 网页功能的设置
*
*/
constructor(browser, options) {
super()
this._browser = browser
this.id = uniqueId("page_")
this.options = options
this.closed = false
this.isFront = false
this.isReady = false
this.container = options.container
this._frames = []
this._target = null
}
/**
* 初始化函数
*
* @return {Promise<undefined>}
*/
init() {
this.build()
this._mainFrame = new Frame(this, this.webview, {
UUID: "~",
routingId: this.webContentsId,
isMainFrame: true,
})
}
/**
* 构建函数
*
* @return {Promise<undefined>}
*/
build() {
const {startUrl, startUrlReferrer, preload, webpreferences} = this.options
const partition = this.options.partition
const webview = document.createElement("webview")
webview.partition = partition
if (startUrlReferrer) {
webview.httpreferrer = startUrlReferrer
}
webview.src = startUrl || "about:blank"
webview.nodeintegrationinsubframes = true
webview.nodeintegration = false
webview.preload = preload
webview.webpreferences = webpreferences
this.container.appendChild(webview)
this.webview = webview
this.session = remote.session.fromPartition(partition)
this.webRequest = this.session.webRequest
this.ipc = new BoundIpc(webview, "*")
this._listenFramesRegister()
this._listenFramesUnregister()
this._bindWebviewEvent()
this._bindIPCEvent()
webview.addEventListener("dom-ready", () => {
/**
* 页面的dom加载完毕
* @event Page#dom-ready
* @type {Object}
* @property {string} url 页面url
*/
this.emit("dom-ready", {url: this.url()})
if (!this.isReady) {
this.isReady = true
if (this.options.devtools) {
webview.openDevTools()
}
this._bindContextMenu()
}
})
}
_waitForReady() {
return new TimeoutPromise((resolve) => {
if (this.isReady) {
resolve(true)
} else {
this.webview.addEventListener("dom-ready", resolve)
}
}, this.options.loadingTimeout || 1e4)
}
// 监听页面的iframe的注册事件
_listenFramesRegister() {
this.ipc.on("frame.register", (frameInfo) => {
if (!frameInfo.isMainFrame) {
let originInfo = this._frames.find(
(item) => item.UUID === frameInfo.UUID
)
if (originInfo) {
Object.assign(originInfo, frameInfo)
} else {
this._frames.push(frameInfo)
}
let originRouting = this._frames.find(
(item) => item.routingId === frameInfo.routingId
)
/**
* 当iframe被首次加载时触发
* @event Page#frameattached
* @type {Object}
* @property {string} name iframe的name
* @property {string} url iframe的url
*/
/**
* 当iframe发生跳转时触发
* @event Page#framenavigated
* @internal
* @type {Object}
* @property {string} name iframe的name
* @property {string} url iframe的url
*/
this.emit(originRouting ? "framenavigated" : "frameattached", frameInfo)
} else {
// mainFrame的_webContentsId
this.webContentsId = frameInfo.routingId
if (this._mainFrame) {
this._mainFrame.webContentsId = frameInfo.routingId
}
/**
* 当和页面建立起连接时触发
* 页面跳转之前会断开连接,刷新或跳转完成后会再次建立连接
* 在domcontentloaded事件之前
* @event Page#connect
* @internal
* @type {Object}
* @property {string} url 页面的url
*/
this.emit("connect", {
url: frameInfo.url,
})
}
this.emit("frame.register", frameInfo)
})
}
// 监听页面的iframe的注销事件
_listenFramesUnregister() {
this.ipc.on("frame.unregister", (frameInfo) => {
if (!frameInfo.isMainFrame) {
this._frames = this._frames.filter(
(item) => item.UUID !== frameInfo.UUID
)
/**
* 当iframe被删除时触发
* @event Page#framedetached
* @type {Object}
* @property {string} name iframe的name
* @property {string} url iframe的url
*/
this.emit("framedetached", frameInfo)
} else {
/**
* 当页面断开连接时触发
* @event Page#disconnect
* @type {Object}
* @property {string} url 当前页面的url
*/
this.emit("disconnect", {
url: frameInfo.url,
})
}
})
}
// 转发webview的dom事件
_proxyDOMEvent(originName, emitName, modifyEvent, isMainFrame) {
this.webview.addEventListener(originName, (evt) => {
if (!isMainFrame || evt.isMainFrame) {
modifyEvent && modifyEvent(evt)
this.emit(emitName, evt)
}
})
return this
}
// 转发ipc事件
_prxoyIPCEvent(
originName,
emitName,
modifyPayload,
isMainFrame,
notMainFrame
) {
this.ipc.on(originName, (payload) => {
if (
payload &&
(!isMainFrame || payload.isMainFrame) &&
(!notMainFrame || !payload.isMainFrame)
) {
modifyPayload && modifyPayload(payload)
this.emit(emitName, payload)
}
})
return this
}
// 需要绑定的快捷键
_injectShortcuts(shortcuts) {
this.shortcuts = shortcuts
}
// 监听webview的dom事件
_bindWebviewEvent() {
/**
* proxy webview Event:"did-start-loading"
* @event Page#load-start
*/
/**
* proxy webview Event:"did-fail-load"
* @event Page#load-fail
*/
/**
* proxy webview Event:"did-stop-loading", Event:"did-finish-load", Event:"did-frame-finish-load"(isMainFrame=true)
* @event Page#load-end
*/
/** 页面标题更新
* @event Page#title-updated
* @type {Object}
* @property {string} title
*/
/** icon更新
* @event Page#favicon-updated
* @type {Object}
* @property {string} favicon
*/
/** proxy webview Event:"console-message"
* @event Page#console
*/
/** proxy webview Event:"new-window"
* @event Page#new-window
*/
this._proxyDOMEvent("did-start-loading", "load-start")
._proxyDOMEvent("did-fail-load", "load-fail")
._proxyDOMEvent("did-stop-loading", "load-end")
._proxyDOMEvent("did-finish-load", "load-end")
._proxyDOMEvent("did-frame-finish-load", "load-end", null, true)
._proxyDOMEvent("page-title-updated", "title-updated")
._proxyDOMEvent("favicon-updated", "favicon-updated", (evt) => {
evt.favicon = evt.favicons.pop()
})
._proxyDOMEvent("console-message", "console")
._proxyDOMEvent("new-window", "new-window")
}
// 监听ipc事件
_bindIPCEvent() {
/**
* iframe onload时触发
* @event Page#load
* @type {Object}
* @property {string} name iframe的name
* @property {string} url iframe的url
*/
/**
* iframe domcontentloaded时触发
* @event Page#domcontentloaded
* @type {Object}
* @property {string} name iframe的name
* @property {string} url iframe的url
*/
this._prxoyIPCEvent("page.title", "title-updated")
._prxoyIPCEvent("page.favicon", "favicon-updated")
._prxoyIPCEvent("frame.load", "load", null, true)
._prxoyIPCEvent("frame.domcontentloaded", "domcontentloaded", null, true)
// iframe和页面加载完成后会请求快捷键
this.ipc.on("page.shortcuts.call", () => {
return (this.shortcuts || []).map((item) => ({...item, callback: null}))
})
// 监听快捷键绑定回复
this.ipc.on("page.shortcuts.trigger", (payload) => {
;(this.shortcuts || []).map((item) => {
if (
(item.action && payload.action === item.action) ||
item.keys.toString() === payload.keys.toString()
) {
item.callback.call(this)
}
})
})
}
// 绑定右键菜单
_bindContextMenu() {
contextMenu({
prepend: (defaultActions, params) => [
{
label: "Open Link in new Tab",
visible: params.linkURL.length !== 0 && params.mediaType === "none",
click: () => {
this.browser().newPage(params.linkURL, this.url())
},
},
],
window: this.webview,
})
}
// 不可直接调用
// 取消激活
_doBack() {
this.isFront = false
this.webview.style.display = "none"
/**
* 当前page取消激活时触发
* @event Page#back
*/
this.emit("back")
}
// 不可直接调用
// 激活
_doFront() {
this.isFront = true
this.webview.style.display = "flex"
/**
* 当前page激活时触发
* @event Page#front
*/
this.emit("front")
}
/**
* 是否在loading状态
*
* @return {boolean}
*/
isLoading() {
return this.webview.isLoading()
}
/**
* 主页面是否在loading状态
*
* @return {boolean}
*/
isLoadingMainFrame() {
return this.webview.isLoadingMainFrame()
}
/**
* 激活当前页面
*
* @return {Promise<this>}
*/
async bringToFront() {
if (!this.isFront) {
this._browser._bringPageToFront(this.id)
}
return this
}
/**
* 获取当前page所属的browser
*
* @return {Browser}
*/
browser() {
return this._browser
}
/**
* 关闭当前page
*
* @return {Browser}
*/
close() {
this.container.removeChild(this.webview)
this._browser._removePage(this.id)
/**
* 当前page关闭时触发
* @event Page#close
*/
this.emit("close")
}
/**
* 获取指定多个url下的cookie数据
* @param {string[]} urls url集合
*
* @return {Cookie[]} Cookie信息集合
*/
cookies(urls) {
return Promise.all(
urls.map((url) => this.session.cookies.get({url}))
).then((cookiesArray) => [].concat(...cookiesArray))
}
/**
* 删除cookie
* 和puppeteer区别的是只支持url和name属性
* @param {...Cookie} cookies 要删除的cookie属性
* @property {string} Cookie.url 与cookie关联的 URL
* @property {string} Cookie.name cookie名称
*
*/
deleteCookies(...cookies) {
return Promise.all(
cookies.map((cookie) =>
this.session.cookies.remove(cookie.url, cookie, name)
)
)
}
/**
* 获取当前page下的所有frame集合,包含mainFrame和iframe
*
* @return {Frame[]}
*/
frames() {
// 递归挂载父的frame信息
const mountParent = (info) => {
if (info.isMainFrame) {
info.parent = null
} else {
let parent = this._frames.find((item) => item.UUID === info.parentIdfa)
info.parent = parent && mountParent(parent)
}
return info
}
return this._frames.map(
(info) => new Frame(this, this.webview, mountParent(info))
)
}
/**
* 当前page是否可后退
*
* @return {boolean}
*/
canGoBack() {
return this.webview.canGoBack()
}
/**
* 当前page是否可前进
*
* @return {boolean}
*/
canGoForward() {
return this.webview.canGoForward()
}
/**
* page后退
*
* @return {undefined}
*/
goBack() {
return this.webview.goBack()
}
/**
* page前进
*
* @return {undefined}
*/
goForward() {
return this.webview.goForward()
}
/**
* 当前page是否关闭
*
* @return {boolean}
*/
isClosed() {
return this.closed
}
/**
* 获取mainFrame
*
* @return {Frame}
*/
mainFrame() {
return this._mainFrame
}
/**
* 搜索页面内是否存在指定文本
* @param {string} text 要搜索的文本
*
* @return {Promise<boolean>}
*/
find(text) {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
onFound({})
}, 500)
const onFound = ({result}) => {
clearTimeout(timeout)
this.webview.removeEventListener("found-in-page", onFound)
this.webview.stopFindInPage("clearSelection")
resolve(result && result.matches > 0)
}
this.webview.addEventListener("found-in-page", onFound)
this.webview.findInPage(text, {
matchCase: true,
})
})
}
/**
* 刷新页面
* 暂不支持options
*/
reload() {
return this.webview.reload()
}
/**
* 指定区域的截图
* 调用webview.capturePage
* @param {Object} rect x, y, width, height属性
*
* @return {Promise<NativeImage>}
*/
screenshot(rect) {
return this.webview.capturePage(rect)
}
/**
* 设置cookie
* @param {...Cookie} cookies
*
* @return {Promise}
*/
setCookie(...cookies) {
return Promise.all(
cookies.map((cookie) => this.session.cookies.set(cookie))
)
}
/**
* todo
* 等待页面发起指定请求
*
* @return {Promise<never>}
*/
waitForRequest(/* urlOrPredicate, options */) {
return Promise.reject("todo")
}
/**
* todo
* 等待页面的指定请求返回
*
* @return {Promise<never>}
*/
waitForResponse(/* urlOrPredicate, options */) {
return Promise.reject("todo")
}
/**
* todo
*
* @return {Promise<never>}
*/
evaluateHandle() {
return Promise.reject("todo")
}
/**
* todo
*
* @return {Promise<never>}
*/
queryObjects() {
return Promise.reject("todo")
}
_setTarget(target) {
this._target = target
}
/**
* 返回当前页面的target
*/
target() {
return this._target
}
}
export default proxyBindDecorator(
[
/**
* [page.mainFrame().document.$ 的简写]{@link ElementHandle#$}
* @method Page#$
*/
"$",
/**
* [page.mainFrame().document.$$ 的简写]{@link ElementHandle#$$}
* @method Page#$$
*/
"$$",
/**
* [page.mainFrame().document.$eval 的简写]{@link ElementHandle#$eval}
* @method Page#$eval
*/
"$eval",
/**
* [page.mainFrame().document.$$eval 的简写]{@link ElementHandle#$$eval}
* @method Page#$$eval
*/
"$$eval",
/**
* [page.mainFrame().document.$x 的简写]{@link ElementHandle#$x}
* @method Page#$x
*/
"$x",
/**
* [page.mainFrame().addScriptTag的简写]{@link Frame#addScriptTag}
* @method Page#addScriptTag
*/
"addScriptTag",
/**
* [page.mainFrame().addStyleTag的简写]{@link Frame#addStyleTag}
* @method Page#addStyleTag
*/
"addStyleTag",
/**
* [page.mainFrame().click的简写]{@link Frame#click}
* @method Page#click
*/
"click",
/**
* [page.mainFrame().content的简写]{@link Frame#content}
* @method Page#content
*/
"content",
/**
* [page.mainFrame().evaluate的简写]{@link Frame#evaluate}
* @method Page#evaluate
*/
"evaluate",
/**
* [page.mainFrame().focus的简写]{@link Frame#focus}
* @method Page#focus
*/
"focus",
/**
* [page.mainFrame().hover的简写]{@link Frame#hover}
* @method Page#hover
*/
"hover",
/**
* [page.mainFrame().goto的简写]{@link Frame#goto}
* @method Page#goto
*/
"goto",
/**
* [page.mainFrame().select的简写]{@link Frame#select}
* @method Page#select
*/
"select",
/**
* [page.mainFrame().setContent的简写]{@link Frame#setContent}
* @method Page#setContent
*/
"setContent",
/**
* [page.mainFrame().tap的简写]{@link Frame#tap}
* @method Page#tap
*/
"tap",
/**
* [page.mainFrame().title的简写]{@link Frame#title}
* @method Page#title
*/
"title",
/**
* [page.mainFrame().type的简写]{@link Frame#type}
* @method Page#type
*/
"type",
/**
* [page.mainFrame().url的简写]{@link Frame#url}
* @method Page#url
*/
"url",
/**
* [page.mainFrame().waitFor的简写]{@link Frame#waitFor}
* @method Page#waitFor
*/
"waitFor",
/**
* [page.mainFrame().waitForFunction的简写]{@link Frame#waitForFunction}
* @method Page#waitForFunction
*/
"waitForFunction",
/**
* [page.mainFrame().waitForNavigation的简写]{@link Frame#waitForNavigation}
* @method Page#waitForNavigation
*/
"waitForNavigation",
/**
* [page.mainFrame().waitForSelector的简写]{@link Frame#waitForSelector}
* @method Page#waitForSelector
*/
"waitForSelector",
/**
* [page.mainFrame().waitForXPath的简写]{@link Frame#waitForXPath}
* @method Page#waitForXPath
*/
"waitForXPath",
/**
* [page.mainFrame().waitForSrcScript的简写]{@link Frame#waitForSrcScript}
* @method Page#waitForSrcScript
*/
"waitForSrcScript",
/**
* [page.mainFrame().localStorageKeys的简写]{@link Frame#localStorageKeys}
* @method Page#localStorageKeys
*/
"localStorageKeys",
/**
* [page.mainFrame().localStorageGet的简写]{@link Frame#localStorageGet}
* @method Page#localStorageGet
*/
"localStorageGet",
/**
* [page.mainFrame().localStorageSet的简写]{@link Frame#localStorageSet}
* @method Page#localStorageSet
*/
"localStorageSet",
/**
* [page.mainFrame().localStorageRemove的简写]{@link Frame#localStorageRemove}
* @method Page#localStorageRemove
*
*/
"localStorageRemove",
],
function () {
return this._mainFrame
}
)(Page)