通过 AJAX 加载 SPA 网页

Posted

技术标签:

【中文标题】通过 AJAX 加载 SPA 网页【英文标题】:Load a SPA webpage via AJAX 【发布时间】:2018-01-22 14:35:04 【问题描述】:

我正在尝试通过插入 URL 来使用 javascript 获取整个网页。但是,该网站是作为单页应用程序 (SPA) 构建的,它使用 JavaScript / backbone.js 在呈现初始响应后动态加载其大部分内容。

例如,当我路由到以下地址时:

https://connect.garmin.com/modern/activity/1915361012

然后在控制台中输入这个(页面加载后):

var $page = $("html")
console.log("%c✔: ", "color:green;", $page.find(".inline-edit-target.page-title-overflow").text().trim());
console.log("%c✔: ", "color:green;", $page.find("footer .details").text().trim());

然后我会得到动态加载的活动标题以及静态加载的页脚:


但是,当我尝试使用 $.get().load() 通过 AJAX 调用加载网页时,我只会收到初始响应(与查看时的内容相同 -来源):

view-source:https://connect.garmin.com/modern/activity/1915361012

因此,如果我使用以下任一 AJAX 调用:

// jQuery.get()
var url = "https://connect.garmin.com/modern/activity/1915361012";
jQuery.get(url,function(data) 
    var $page = $("<div>").html(data)
    console.log("%c✖: ", "color:red;",   $page.find(".page-title").text().trim());
    console.log("%c✔: ", "color:green;", $page.find("footer .details").text().trim());
);

// jQuery.load()
var url = "https://connect.garmin.com/modern/activity/1915361012";
var $page = $("<div>")
$page.load(url, function(data) 
    console.log("%c✖: ", "color:red;",   $page.find(".page-title").text().trim()    );
    console.log("%c✔: ", "color:green;", $page.find("footer .details").text().trim());
);

我仍会获得初始页脚,但不会获得任何其他页面内容:


我已经尝试了 solution here 到 eval() 的每个 script 标记的内容,但这看起来不够健壮,无法实际加载页面:

jQuery.get(url,function(data) 
    var $page = $("<div>").html(data)
    $page.find("script").each(function() 
        var scriptContent = $(this).html(); //Grab the content of this tag
        eval(scriptContent); //Execute the content
    );
    console.log("%c✖: ", "color:red;",   $page.find(".page-title").text().trim());
    console.log("%c✔: ", "color:green;", $page.find("footer .details").text().trim());
);

问:是否有任何选项可以完全加载可通过 JavaScript 抓取的网页?

【问题讨论】:

最终目标是什么?如果您想获取数据,也许直接访问原始数据更容易(取决于您的来源,显然如果您知道您的 ID,您可以在没有 cookie 或任何东西的情况下获取数据like that)。如果你真的想加载整个页面然后从 DOM 中挖掘数据,唯一通用的解决方案是使用“可测量”的无头浏览器,例如 PhantomJS 或 Headless Chrome 【参考方案1】:

您永远无法完全复制任意 (SPA) 页面的功能。

我看到的唯一方法是使用无头浏览器,例如 PhantomJS 或 Headless Chrome 或 Headless Firefox。

我想尝试 Headless Chrome,所以让我们看看它可以对您的页面做什么:

使用内部 REPL 快速检查

使用 Chrome Headless 加载该页面(您需要在 Mac/Linux 上使用 Chrome 59,在 Windows 上需要 Chrome 60),并使用来自 REPL 的 JavaScript 查找页面标题:

% chrome --headless --disable-gpu --repl https://connect.garmin.com/modern/activity/1915361012
[0830/171405.025582:INFO:headless_shell.cc(303)] Type a Javascript expression to evaluate or "quit" to exit.
>>> $('body').find('.page-title').text().trim() 
"result":"type":"string","value":"Daily Mile - Round 2 - Day 27"

注意:要让chrome 命令行在 Mac 上运行,我事先已经这样做了:

alias chrome="'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'"

以编程方式使用 Node 和 Puppeteer

Puppeteer 是一个 Node 库(由 Google Chrome 开发人员提供),它提供了一个高级 API 来通过 DevTools 协议控制无头 Chrome。它还可以配置为使用完整(非无头)Chrome。

(第 0 步:安装 Node 和 Yarn 如果你没有它们)

在新目录中:

yarn init
yarn add puppeteer

用这个创建index.js

const puppeteer = require('puppeteer');
(async() => 
    const url = 'https://connect.garmin.com/modern/activity/1915361012';
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    // Go to URL and wait for page to load
    await page.goto(url, waitUntil: 'networkidle');
    // Wait for the results to show up
    await page.waitForSelector('.page-title');
    // Extract the results from the page
    const text = await page.evaluate(() => 
        const title = document.querySelector('.page-title');
        return title.innerText.trim();
    );
    console.log(`Found: $text`);
    browser.close();
)();

结果:

$ node index.js 
Found: Daily Mile - Round 2 - Day 27

【讨论】:

【参考方案2】:

首先:避免eval - 你的内容安全策略应该阻止它,它会让你容易受到 XSS 攻击。抓取机器人肯定不会运行它。

您所描述的问题对于所有 SPA 来说都是常见的 - 当一个人访问时,他们会获得您的应用程序 shell 脚本,然后加载其余内容 - 一切都很好。当机器人访问时,它们会忽略脚本并返回空壳。

解决方案是服务器端渲染。一种方法是,如果您在服务器上使用 JS 渲染器(例如 React)和 Node.js,您可以相当轻松地构建 JS 并静态提供它。

但是,如果您不是,那么您需要在您的服务器上运行一个无头浏览器,该浏览器执行用户将执行的所有 JS,然后将结果提供给机器人。

幸运的是,其他人已经有done all the work here。他们在网上放了一个演示,你可以try out with your site:

【讨论】:

我同意避免 eval,但最终需要一种方法来触发加载页面内容的脚本。 Garmin.com 不是 我的 站点,因此我无法启用任何服务器端渲染解决方案。 Renderton 似乎并没有像简单地导航到页面那样实际加载后续内容,至少在尝试几次之后不会(即使您包含的屏幕截图也是空白的)。但是无头浏览器可能值得探索。 @KyleMit 你不需要eval。代替 AJAX 将 &lt;script&gt; 标记添加到引用您要执行的脚本的页面。我不推荐它,但你可以使用$.getScript in jQuery。 @KyleMit 您可以使用无头浏览器来呈现其他人的网站,但您可能需要额外的步骤。 Rendertron 可以处理 Shadow DOM,但 Garmin 站点也在相当大的 D3 和 Map API JS 库中延迟加载,因此您需要等待这些库完成,然后再创建静态副本(Rendertron 不会等待默认)【参考方案3】:

我想你应该知道SPA的概念, SPA 是单页应用程序,它只是静态 html 文件。当路由发生变化时,页面会动态创建或修改DOM节点,以达到使用Javascript切换页面的效果。

因此,如果你使用$.get(),服务器会响应一个静态的html文件,该文件有一个稳定的页面,所以你不会加载你想要的。

如果你想使用$.get(),它有两种方式,第一种是使用headless browser,例如headless chromephantomJS等。它会帮助你加载页面,你可以得到@ 987654331@加载页面的节点。第二个是s-s-rServer Slide Render),如果你使用s-s-r,你会直接通过$.get获取页面的HTML数据,因为服务器响应时响应页面的HTML数据请求不同的路线。

参考:

s-s-r

vue的SRR框架:Nuxt.js

PhantomJS

Node API of Headless Chrome

【讨论】:

以上是关于通过 AJAX 加载 SPA 网页的主要内容,如果未能解决你的问题,请参考以下文章

Ajax 使用ajax加上get和post方法,通过后台加载数据,并在网页上进行显示案例+解释

前后端分离

对Ajax的解析

如何通过导航菜单 ajax-refresh 动态包含内容? (JSF SPA)

怎么解决SPA首屏加载速度慢?

ajax连接数据库加载+三级联动