From 37ad92e0d7c4e2199ad3555dcea7b99517e5e19a Mon Sep 17 00:00:00 2001 From: Zeke Lu Date: Fri, 4 Jun 2021 19:23:37 +0800 Subject: [PATCH] replace Evaluate with CallFunctionOn to execuate js on node Note: there is a performance cost here. Before the changes, just one CDP command is sent: - Runtime.evaluate After the changes, three CDP command are sent: - DOM.resolveNode: to get the RemoteObjectId from NodeId - Runtime.callFunctionOn - Runtime.releaseObject --- js.go | 124 ++++++++++++++++++------------------------------------- query.go | 50 +++++++++++++--------- 2 files changed, 70 insertions(+), 104 deletions(-) diff --git a/js.go b/js.go index 2a8c4989..39b5b50f 100644 --- a/js.go +++ b/js.go @@ -1,89 +1,76 @@ package chromedp -import ( - "fmt" - - "github.com/chromedp/cdproto/cdp" -) - const ( - // textJS is a javascript snippet that returns the concatenated innerText of all - // visible (ie, offsetWidth || offsetHeight || getClientRects().length ) children. - textJS = `(function(a) { - var s = ''; - for (var i = 0; i < a.length; i++) { - if (a[i].offsetWidth || a[i].offsetHeight || a[i].getClientRects().length) { - s += a[i].innerText; - } + // textJS is a javascript snippet that returns the innerText of the specified + // visible (ie, offsetWidth || offsetHeight || getClientRects().length ) element. + textJS = `function text() { + if (this.offsetWidth || this.offsetHeight || this.getClientRects().length) { + return this.innerText; } - return s; - })(%s)` + return ''; + }` - // textContentJS is a javascript snippet that returns the concatenated textContent - // of all children. - textContentJS = `(function(a) { - var s = ''; - for (var i = 0; i < a.length; i++) { - s += a[i].textContent; - } - return s; - })(%s)` + // textContentJS is a javascript snippet that returns the textContent of the + // specified element. + textContentJS = `function textContent() { + return this.textContent; + }` - // blurJS is a javscript snippet that blurs the specified element. - blurJS = `(function(a) { - a.blur(); + // blurJS is a javascript snippet that blurs the specified element. + blurJS = `function blur() { + this.blur(); return true; - })(%s)` + }` // submitJS is a javascript snippet that will call the containing form's // submit function, returning true or false if the call was successful. - submitJS = `(function(a) { - if (a.nodeName === 'FORM') { - HTMLFormElement.prototype.submit.call(a); + submitJS = `function submit() { + if (this.nodeName === 'FORM') { + HTMLFormElement.prototype.submit.call(this); return true; - } else if (a.form !== null) { - HTMLFormElement.prototype.submit.call(a.form); + } else if (this.form !== null) { + HTMLFormElement.prototype.submit.call(this.form); return true; } return false; - })(%s)` + }` // resetJS is a javascript snippet that will call the containing form's // reset function, returning true or false if the call was successful. - resetJS = `(function(a) { - if (a.nodeName === 'FORM') { - HTMLFormElement.prototype.reset.call(a); + resetJS = `function reset() { + if (this.nodeName === 'FORM') { + HTMLFormElement.prototype.reset.call(this); return true; - } else if (a.form !== null) { - HTMLFormElement.prototype.reset.call(a.form); + } else if (this.form !== null) { + HTMLFormElement.prototype.reset.call(this.form); return true; } return false; - })(%s)` + }` // attributeJS is a javascript snippet that returns the attribute of a specified // node. - attributeJS = `(function(a, n) { - return a[n]; - })(%s, %q)` + attributeJS = `function attribute(n) { + return this[n]; + }` // setAttributeJS is a javascript snippet that sets the value of the specified // node, and returns the value. - setAttributeJS = `(function(a, n, v) { - a[n] = v; + setAttributeJS = `function setAttribute(n, v) { + this[n] = v; if (n === 'value') { - a.dispatchEvent(new Event('input', { bubbles: true })); - a.dispatchEvent(new Event('change', { bubbles: true })); + this.dispatchEvent(new Event('input', { bubbles: true })); + this.dispatchEvent(new Event('change', { bubbles: true })); } - return a[n]; - })(%s, %q, %q)` + return this[n]; + }` // visibleJS is a javascript snippet that returns true or false depending on if // the specified node's offsetWidth, offsetHeight or getClientRects().length is // not null. - visibleJS = `(function(a) { - return Boolean( a.offsetWidth || a.offsetHeight || a.getClientRects().length ); - })(%s)` + visibleJS = `function visible() { + return Boolean( this.offsetWidth || this.offsetHeight || this.getClientRects().length ); + }` // waitForPredicatePageFunction is a javascript snippet that runs the polling in the // browser. It's copied from puppeteer. See @@ -168,34 +155,3 @@ const ( } }` ) - -// snippet builds a Javascript expression snippet. -func snippet(js string, f func(n *cdp.Node) string, sel interface{}, n *cdp.Node, v ...interface{}) string { - switch s := sel.(type) { - case *Selector: - if s != nil && s.raw { - return fmt.Sprintf(js, append([]interface{}{s.selAsString()}, v...)...) - } - } - return fmt.Sprintf(js, append([]interface{}{f(n)}, v...)...) -} - -// cashX returns the $x() expression using the node's full xpath value. -func cashX(flatten bool) func(*cdp.Node) string { - return func(n *cdp.Node) string { - if flatten { - return fmt.Sprintf(`$x(%q)[0]`, n.PartialXPath()) - } - return fmt.Sprintf(`$x(%q)`, n.PartialXPath()) - } -} - -// cashXNode returns the $x(/node()) expression using the node's full xpath value. -func cashXNode(flatten bool) func(*cdp.Node) string { - return func(n *cdp.Node) string { - if flatten { - return fmt.Sprintf(`$x(%q)[0]`, n.PartialXPath()+"/node()") - } - return fmt.Sprintf(`$x(%q)`, n.PartialXPath()+"/node()") - } -} diff --git a/query.go b/query.go index abc71f31..7e0900ed 100644 --- a/query.go +++ b/query.go @@ -34,7 +34,6 @@ type Selector struct { by func(context.Context, *cdp.Node) ([]cdp.NodeID, error) wait func(context.Context, *cdp.Frame, runtime.ExecutionContextID, ...cdp.NodeID) ([]*cdp.Node, error) after func(context.Context, runtime.ExecutionContextID, ...*cdp.Node) error - raw bool } // Query is a query action that queries the browser for specific element @@ -358,7 +357,6 @@ func BySearch(s *Selector) { // Note: Do not use with an untrusted selector value, as any defined selector // will be passed to runtime.Evaluate. func ByJSPath(s *Selector) { - s.raw = true ByFunc(func(ctx context.Context, n *cdp.Node) ([]cdp.NodeID, error) { // set up eval command p := runtime.Evaluate(s.selAsString()). @@ -425,15 +423,28 @@ func NodeReady(s *Selector) { WaitFunc(s.waitReady(nil))(s) } -func withContextID(id runtime.ExecutionContextID) EvaluateOption { - return func(p *runtime.EvaluateParams) *runtime.EvaluateParams { - return p.WithContextID(id) +func callFunctionOnNode(ctx context.Context, node *cdp.Node, function string, res interface{}, args ...interface{}) error { + r, err := dom.ResolveNode().WithNodeID(node.NodeID).Do(ctx) + if err != nil { + return err } -} + err = CallFunctionOn(function, &res, + func(p *runtime.CallFunctionOnParams) *runtime.CallFunctionOnParams { + return p.WithObjectID(r.ObjectID) + }, + args..., + ).Do(ctx) + + if err != nil { + return err + } + + // Try to release the remote object. + // It will fail if the page is navigated or closed, + // and it's okay to ignore the error in this case. + _ = runtime.ReleaseObject(r.ObjectID).Do(ctx) -func evalInCtx(ctx context.Context, execCtx runtime.ExecutionContextID, expression string, res interface{}, opts ...EvaluateOption) error { - allOpts := append([]EvaluateOption{withContextID(execCtx)}, opts...) - return EvaluateAsDevTools(expression, &res, allOpts...).Do(ctx) + return nil } // NodeVisible is an element query option to wait until all queried element @@ -452,7 +463,7 @@ func NodeVisible(s *Selector) { // check visibility var res bool - err = evalInCtx(ctx, execCtx, snippet(visibleJS, cashX(true), s, n), &res, withContextID(execCtx)) + err = callFunctionOnNode(ctx, n, visibleJS, &res) if err != nil { return err } @@ -479,7 +490,7 @@ func NodeNotVisible(s *Selector) { // check visibility var res bool - err = evalInCtx(ctx, execCtx, snippet(visibleJS, cashX(true), s, n), &res) + err = callFunctionOnNode(ctx, n, visibleJS, &res) if err != nil { return err } @@ -648,7 +659,7 @@ func Blur(sel interface{}, opts ...QueryOption) QueryAction { } var res bool - err := evalInCtx(ctx, execCtx, snippet(blurJS, cashX(true), sel, nodes[0]), &res) + err := callFunctionOnNode(ctx, nodes[0], blurJS, &res) if err != nil { return err } @@ -689,7 +700,7 @@ func Text(sel interface{}, text *string, opts ...QueryOption) QueryAction { return fmt.Errorf("selector %q did not return any nodes", sel) } - return evalInCtx(ctx, execCtx, snippet(textJS, cashX(false), sel, nodes[0]), text) + return callFunctionOnNode(ctx, nodes[0], textJS, text) }, opts...) } @@ -705,7 +716,7 @@ func TextContent(sel interface{}, text *string, opts ...QueryOption) QueryAction return fmt.Errorf("selector %q did not return any nodes", sel) } - return evalInCtx(ctx, execCtx, snippet(textContentJS, cashX(false), sel, nodes[0]), text) + return callFunctionOnNode(ctx, nodes[0], textContentJS, text) }, opts...) } @@ -931,11 +942,10 @@ func JavascriptAttribute(sel interface{}, name string, res interface{}, opts ... return fmt.Errorf("selector %q did not return any nodes", sel) } - if err := evalInCtx(ctx, execCtx, - snippet(attributeJS, cashX(true), sel, nodes[0], name), res, - ); err != nil { + if err := callFunctionOnNode(ctx, nodes[0], attributeJS, res, name); err != nil { return fmt.Errorf("could not retrieve attribute %q: %w", name, err) } + return nil }, opts...) } @@ -949,7 +959,7 @@ func SetJavascriptAttribute(sel interface{}, name, value string, opts ...QueryOp } var res string - err := evalInCtx(ctx, execCtx, snippet(setAttributeJS, cashX(true), sel, nodes[0], name, value), &res) + err := callFunctionOnNode(ctx, nodes[0], setAttributeJS, &res, name, value) if err != nil { return err } @@ -1109,7 +1119,7 @@ func Submit(sel interface{}, opts ...QueryOption) QueryAction { } var res bool - err := evalInCtx(ctx, execCtx, snippet(submitJS, cashX(true), sel, nodes[0]), &res) + err := callFunctionOnNode(ctx, nodes[0], submitJS, &res) if err != nil { return err } @@ -1131,7 +1141,7 @@ func Reset(sel interface{}, opts ...QueryOption) QueryAction { } var res bool - err := evalInCtx(ctx, execCtx, snippet(resetJS, cashX(true), sel, nodes[0]), &res) + err := callFunctionOnNode(ctx, nodes[0], resetJS, &res) if err != nil { return err }