在任何文档加载/处理之前注入Javascript的CefSharp

10

我正在进行的项目需要在网页文档处理之前注入javascript。使用WebBrowser组件可以轻松实现这一点,但是我在使用CefSharp时遇到了困难。

这是问题的一个简化版本,一个网页需要存在“InjectedObject”才能正常运行。如果在文档的最顶部没有发生注入或在文档被处理之前进行评估/执行,将会导致以下html示例输出:

=====html example output on failure=====

isObjectPresent?

false

=====

然而我需要网页显示如下:

=====html example output on success=====

isObjectPresent?

true

=====

<html>
  <head>
    <script>
      isObjectPresent = typeof InjectedObject == "object";
    </script>
  </head>
  <body>
    <p>isObjectPresent?</p>
    <div id="result"></div>
    <script>
      document.getElementById("result").innerHTML = isObjectPresent;
    </script>
  </body>
</html>

查看所有可用的建议,表明我应该使用LoadingStateChanged()或FrameLoadEnd()来注入脚本,即:

观察到所有可用的建议后,我应该使用LoadingStateChanged()或FrameLoadEnd()来注入脚本。

public void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs args) {
  if (args.Frame.IsMain) {
    args.Frame.ExecuteJavascriptAsync("window.InjectedObject = {};");
  }
}

然而,我尝试的所有迭代版本,甚至使用FrameLoadStart,在文档开始处理之后才出现插入的javascript。是否有真正的javascript注入示例可确保在文档处理开始之前发生(确保避免竞争条件/时间问题)。

作为我想要模仿的WebBrowser组件行为的例子:

private void uiWebBrowser_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e) 
{
  var browser = (WebBrowser)sender;
  var document = browser.Document as HTMLDocument;
  var head = document.getElementsByTagName("head").Cast<HTMLHeadElement>().First();
  if (head != null)
  {
    var script = document.createElement("script") as IHTMLScriptElement;
    script.text = "window.InjectedObject = {};"
    if (head.firstChild != null)
    {
      head.insertBefore((IHTMLDOMNode)script, head.firstChild);
    }
    else
    {
      head.appendChild((IHTMLDOMNode)script;
    }
  }
}

欢迎提供任何帮助或建议,理想情况下我希望避免通过互联网请求解析和插入下载页面,然后使用loadhtml,因为我预计可能需要为所有影响主框架的导航操作执行此操作,这听起来像是一个hack方法。

根据评论的跟进建议,建议使用javascript V8引擎上下文来实现上述用例。尝试从IRenderProcessMessageHandler接口实现OnContextCreated方法具有相同的结果。

==MainWindow.xaml==

<Window x:Class="ExampleCefSharp001.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:cefSharp="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ExampleCefSharp001"
    mc:Ignorable="d"
    Title="MainWindow" Height="1000" Width="1100">
  <Grid>
    <cefSharp:ChromiumWebBrowser x:Name="uiWebView"></cefSharp:ChromiumWebBrowser>
  </Grid>
</Window>

==MainWindow.xaml.cs==

public partial class MainWindow : Window
{
  JavascriptManager jsmanager;

  public MainWindow()
  {
    InitializeComponent();

    jsmanager = new JavascriptManager(uiWebView);
  }
}

public class JavascriptManager : ILoadHandler, IRenderProcessMessageHandler
{
  string injection = "window.InjectedObject = {};";

  public JavascriptManager(ChromiumWebBrowser browser)
  {
    browser.LoadHandler = this;
    browser.RenderProcessMessageHandler = this;

    //  Lets just pretend this is a real url with the example html above.
    browser.Address = "https://www.example.com/timingtest.htm"
  }

  public void OnContextCreated(IWebBrowser browserControl, IBrowser browser, IFrame frame)
  {
    frame.ExecuteJavaScriptAsync(injection);
  }
}

我非常感谢您的评论和建议。如果有我忽略的地方,请告诉我!


请阅读“常见问题解答”,该主题已在其中涵盖。 - amaitland
嗨,Amaitland,我想你在谈论[https://github.com/cefsharp/CefSharp/wiki/Frequently-asked-questions],它指出了许多在运行时注入JavaScript的方法,但没有提到预处理(至少我没看到)。如果有关于预处理的参考资料,我会非常感兴趣知道它在哪里。 - Glorifundel
V8上下文创建很可能已经足够满足您的需求。 - amaitland
嗨,amaitland,我根据您的建议更新了问题,具体是使用V8上下文创建,我认为您的意思是使用OnContextCreated。我甚至添加了MainWindow.xaml(虽然我没有做太多工作)以及MainWindow.xaml.cs代码,以获得更完整的视图。html页面本身就是测试。如果没有将对象分配给window.InjectedObject,则仅在body上显示它不存在。问题在于预处理,在其他浏览器中,我可以在所有其他javascript之前注入javascript。 - Glorifundel
如果我误解了,请原谅,但我不确定为什么你要这么好斗,这个问题的目的是确定是否可以使用cefsharp进行预处理。我确实感谢您的建议。它只是在我提出的测试案例中没有起作用,我知道通过查看文档/论坛,您活跃在那个社区中,所以您比我更熟悉cefsharp的能力和不能做到的事情。就目前而言,我无法使用cefsharp解决我提出的时间问题,HTML文档始终呈现“false”。 - Glorifundel
显示剩余3条评论
2个回答

6

终于回来了。这篇文章的灵感来源于以下示例:CefSharp.Example/Filters/FindReplaceResponseFilter.cs

实现IRequestHandler和IResponseFilter接口:

==MainWindow.xaml==

<Window x:Class="ExampleCefSharp001.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:cefSharp="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:local="clr-namespace:ExampleCefSharp001"
  mc:Ignorable="d"
  Title="MainWindow" Height="1000" Width="1100">
  <Grid>
    <cefSharp:ChromiumWebBrowser x:Name="uiWebView"></cefSharp:ChromiumWebBrowser>
  </Grid>
</Window>

==MainWindow.xaml.cs==

public partial class MainWindow : Window
{
  JavascriptManager jsmanager;

  public MainWindow()
  {
    InitializeComponent();

    jsmanager = new JavascriptManager(uiWebView);
  }
}

public class JavascriptManager : IRequestHandler
{
  string injection = "window.InjectedObject = {};";

  public JavascriptManager(ChromiumWebBrowser browser)
  {
    browser.RequestHandler = this;

    //  Lets just pretend this is a real url with the example html above.
    browser.Address = "https://www.example.com/timingtest.htm"
  }

  public IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response)
    {
        if (frame.IsMain && request.ResourceType == ResourceType.MainFrame) 
        {
            return new JavascriptInjectionFilter(injection);
        }
        return null;
    }
}

public class JavascriptInjectionFilter : IResponseFilter
{
    /// <summary>
    /// Location to insert the javascript
    /// </summary>
    public enum Locations
    {
        /// <summary>
        /// Insert Javascript at the top of the header element
        /// </summary>
        head,
        /// <summary>
        /// Insert Javascript at the top of the body element
        /// </summary>
        body
    }

    string injection;
    string location;
    int offset = 0;
    List<byte> overflow = new List<byte>();

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="injection"></param>
    /// <param name="location"></param>
    public JavascriptInjectionFilter(string injection, Locations location = Locations.head)
    {
        this.injection = "<script>" + injection + "</script>";
        switch (location)
        {
            case Locations.head:
                this.location = "<head>";
                break;

            case Locations.body:
                this.location = "<body>";
                break;

            default:
                this.location = "<head>";
                break;
        }
    }

    /// <summary>
    /// Disposal
    /// </summary>
    public void Dispose()
    {
        //
    }

    /// <summary>
    /// Filter Processing...  handles the injection
    /// </summary>
    /// <param name="dataIn"></param>
    /// <param name="dataInRead"></param>
    /// <param name="dataOut"></param>
    /// <param name="dataOutWritten"></param>
    /// <returns></returns>
    public FilterStatus Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten)
    {
        dataInRead = dataIn == null ? 0 : dataIn.Length;
        dataOutWritten = 0;

        if (overflow.Count > 0)
        {
            var buffersize = Math.Min(overflow.Count, (int)dataOut.Length);
            dataOut.Write(overflow.ToArray(), 0, buffersize);
            dataOutWritten += buffersize;

            if (buffersize < overflow.Count)
            {
                overflow.RemoveRange(0, buffersize - 1);
            }
            else
            {
                overflow.Clear();
            }
        }


        for (var i = 0; i < dataInRead; ++i)
        {
            var readbyte = (byte)dataIn.ReadByte();
            var readchar = Convert.ToChar(readbyte);
            var buffersize = dataOut.Length - dataOutWritten;

            if (buffersize > 0)
            {
                dataOut.WriteByte(readbyte);
                dataOutWritten++;
            }
            else
            {
                overflow.Add(readbyte);
            }

            if (char.ToLower(readchar) == location[offset])
            {
                offset++;
                if (offset >= location.Length)
                {
                    offset = 0;
                    buffersize = Math.Min(injection.Length, dataOut.Length - dataOutWritten);

                    if (buffersize > 0)
                    {
                        var data = Encoding.UTF8.GetBytes(injection);
                        dataOut.Write(data, 0, (int)buffersize);
                        dataOutWritten += buffersize;
                    }

                    if (buffersize < injection.Length)
                    {
                        var remaining = injection.Substring((int)buffersize, (int)(injection.Length - buffersize));
                        overflow.AddRange(Encoding.UTF8.GetBytes(remaining));
                    }

                }
            }
            else
            {
                offset = 0;
            }

        }

        if (overflow.Count > 0 || offset > 0)
        {
            return FilterStatus.NeedMoreData;
        }

        return FilterStatus.Done;
    }

    /// <summary>
    /// Initialization
    /// </summary>
    /// <returns></returns>
    public bool InitFilter()
    {
        return true;
    }

}

感谢amaitland给我的指引,以及提供的示例程序,上述代码的大部分都是基于它的。最终结果:
<html><head></head><body><script>window.InjectedObject = {}</script>
  <script>
    isObjectPresent = typeof InjectedObject == "object";
  </script>
  <p>isObjectPresent?</p>
  <div id="result"></div>
  <script>
    document.getElementById("result").innerHTML = isObjectPresent;
  </script>
</body></html>

我需要在标题栏的顶部添加一些文本来预处理文档,确保注入代码之前已经运行现有代码,以避免出现时间问题。

编辑 进行了一些小修正。只有在主机载入时才添加控制逻辑。


示例中还有一些调试代码,我敢肯定如果您看代码 5 分钟就能解决问题。 - amaitland

2

你的回答是正确的,你应该重写GetResourceResponseFilter的实现。但是如果你没有以正确的方式实现接口,你最终会导致浏览器无法呈现内容。相反,你可以继承DefaultRequestHandler并重写GetResourceResponseFilter(),并按照所述的方式提供自定义过滤器。这样做会更容易,特别是在你只需要覆盖这个特定功能的情况下:


public class CustomRequestHandler : DefaultRequestHandler
{
    string script = "alert('hello');";

    public override IResponseFilter GetResourceResponseFilter(CefSharp.IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response)
    {
        if (frame.IsMain && request.ResourceType == ResourceType.MainFrame)
        {
            return new JavascriptInjectionFilter(script);
        }
        return null;
    }
}

然后将其分配给Chromium浏览器:

最初的回答:

CustomRequestHandler customRequestHandler = new CustomRequestHandler();
chromeBrowser.RequestHandler = customRequestHandler ;

最好是您编辑已接受的答案并进行代码更改。谢谢。 - amaitland
DefaultRequestHandler已更名: https://github.com/cefsharp/CefSharp/issues/3124 - user2029101

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接