-
Notifications
You must be signed in to change notification settings - Fork 10
/
index.js
179 lines (162 loc) · 5.94 KB
/
index.js
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
'use strict';
const ChromePoll = require('chrome-pool');
const package_json = require('./package.json');
const ERR_REQUIRE_URL = new Error('url param is required', 1);
const ERR_RENDER_TIMEOUT = new Error('chrome-render timeout', 2);
const ERR_PAGE_LOAD_FAILED = new Error('page load failed', 3);
/**
* a ChromeRender will launch a chrome with some tabs to render web pages.
* use #new() static method to make a ChromeRender, don't use new ChromeRender()
* #new() is a async function, new ChromeRender is use able util await it to be completed
*/
class ChromeRender {
/**
* make a new ChromeRender
* @param {object} params
* {
* maxTab: `number` max tab chrome will open to render pages, default is no limit, `maxTab` used to avoid open to many tab lead to chrome crash.
* chromeRunnerOptions: `object` same as chrome-runner's options, see [https://github.com/gwuhaolin/chrome-runner#options](chrome-runner options)
* }
* @return {Promise.<ChromeRender>}
*/
static async new(params = {}) {
const { maxTab, chromeRunnerOptions } = params;
const chromeRender = new ChromeRender();
chromeRender.chromePoll = await ChromePoll.new({
maxTab,
protocols: ['Page', 'DOM', 'Network', 'Runtime', 'Emulation'],
chromeRunnerOptions
});
return chromeRender;
}
/**
* render page in chrome, and return page html string
* @param params
* {
* url: `string` is required, web page's URL
* cookies: `object {cookieName:cookieValue}` set HTTP cookies when request web page
* headers: `object {headerName:headerValue}` add HTTP headers when request web page
* useReady: `boolean` whether use `window.chromeRenderReady()` to notify chrome-render page has ready. default is false chrome-render use `domContentEventFired` as page has ready.
* script: inject script to evaluate when page on load
* renderTimeout: `number` in ms, `render()` will throw error if html string can't be resolved after `renderTimeout`, default is 5000ms.
* deviceMetricsOverride: `object` Overrides the values of device screen dimensions, same as https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride
* }
* @returns {Promise.<string>} page html string
*/
async render(params) {
let client;
return await new Promise(async (resolve, reject) => {
let timer;
let { url, cookies, headers = {}, useReady, script, renderTimeout = 5000, deviceMetricsOverride } = params;
// params assert
// page url's requires
if (!url) {
return reject(ERR_REQUIRE_URL);
}
// open a tab
client = await this.chromePoll.require();
const { Page, DOM, Network, Emulation, Runtime } = client.protocol;
// add timeout reject
timer = setTimeout(() => {
reject(ERR_RENDER_TIMEOUT);
clearTimeout(timer);
}, renderTimeout);
// get and resolve page HTML string when ready
const resolveHTML = async () => {
try {
const dom = await DOM.getDocument();
const ret = await DOM.getOuterHTML({ nodeId: dom.root.nodeId });
resolve(ret.outerHTML);
} catch (err) {
reject(err);
}
clearTimeout(timer);
};
// inject cookies
if (cookies && typeof cookies === 'object') {
Object.keys(cookies).forEach((name) => {
Network.setCookie({
url: url,
name: name,
value: cookies[name],
});
})
}
// detect page load failed error
let firstRequestId;
Network.requestWillBeSent((params) => {
if (firstRequestId === undefined) {
firstRequestId = params.requestId;
}
});
Network.loadingFailed((params) => {
if (params.requestId === firstRequestId) {
reject(ERR_PAGE_LOAD_FAILED);
}
});
// inject script to evaluate when page on load
if (typeof script === 'string') {
Page.addScriptToEvaluateOnLoad({
scriptSource: script,
});
}
// detect request from chrome-render
Network.setExtraHTTPHeaders({
headers: Object.assign({
'x-chrome-render': package_json.version
}, headers),
});
if (useReady === true) {
Page.frameNavigated(() => {
// define window.isPageReady to listen page ready event
// wait for page ready event to resolveHTML
if (useReady === true) {
// Page.frameNavigated may be fired more than one times
useReady = false;
Runtime.evaluate({
awaitPromise: true,
silent: true,
expression: `
new Promise((fulfill) => {
Object.defineProperty(window, 'isPageReady', {
set: function(value) { document.dispatchEvent(new Event('_crPageRendered')) },
});
document.addEventListener('_crPageRendered', fulfill, {
once: true
});
})`
}).then(resolveHTML)
.catch(reject);
}
});
} else {
Page.domContentEventFired(resolveHTML);
}
// override device metrics
if (deviceMetricsOverride && typeof deviceMetricsOverride === 'object') {
// https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride
Emulation.setDeviceMetricsOverride(deviceMetricsOverride);
}
// to go page
await Page.navigate({
url,
referrer: headers['referrer']
});
}).then((html) => {
this.chromePoll.release(client.tabId, params.clearTab);
return Promise.resolve(html);
}).catch((err) => {
this.chromePoll.release(client.tabId, params.clearTab);
return Promise.reject(err);
});
}
/**
* destroyPoll this chrome render, kill chrome, release all resource
* @returns {Promise.<void>}
*/
async destroyRender() {
await this.chromePoll.destroyPoll();
this.chromePoll = null;
}
}
module.exports = ChromeRender;